//! Comprehensive test suite for LSP server functionality use document::Document; use tower_lsp::lsp_types::*; use super::*; // Test data fixtures const SAMPLE_STORYBOOK: &str = r#" species Human { intelligence: high lifespan: 80 } character Alice: Human { age: 7 ---backstory A curious girl who loves adventures --- } template Child { age: 5..12 guardian: Human } character Bob: Human from Child { age: 10 guardian: Alice } life_arc Growing { state child { on enter { age: 5 } } state teen { on enter { age: 13 } } state adult { on enter { age: 18 } } } schedule DailyRoutine { 08:00 -> 09:00: breakfast { activity: eating } 09:00 -> 12:00: school { activity: learning } } relationship Friendship { Alice as friend { bond_strength: 5 } Bob as friend { bond_strength: 5 } } "#; #[cfg(test)] mod document_tests { use super::*; #[test] fn test_document_creation() { let doc = Document::new(SAMPLE_STORYBOOK.to_string()); if !doc.parse_errors.is_empty() { eprintln!("Parse errors:"); for err in &doc.parse_errors { eprintln!(" - {}", err.message); } } assert_eq!(doc.text, SAMPLE_STORYBOOK); assert!(doc.ast.is_some(), "AST should be parsed"); } #[test] fn test_document_with_errors() { let invalid = "character { invalid syntax }"; let doc = Document::new(invalid.to_string()); assert!(doc.ast.is_none(), "Invalid syntax should not produce AST"); assert!(!doc.parse_errors.is_empty(), "Should have parse errors"); } #[test] fn test_document_update() { let mut doc = Document::new("character Alice {}".to_string()); doc.update("character Bob {}".to_string()); assert_eq!(doc.text, "character Bob {}"); assert!(doc.name_table.resolve_name("Bob").is_some()); assert!(doc.name_table.resolve_name("Alice").is_none()); } #[test] fn test_symbol_extraction() { let doc = Document::new(SAMPLE_STORYBOOK.to_string()); assert!(doc.name_table.resolve_name("Alice").is_some()); assert!(doc.name_table.resolve_name("Bob").is_some()); assert!(doc.name_table.resolve_name("Child").is_some()); assert!(doc.name_table.resolve_name("Growing").is_some()); assert!(doc.name_table.resolve_name("DailyRoutine").is_some()); assert!(doc.name_table.resolve_name("Human").is_some()); assert!(doc.name_table.resolve_name("Friendship").is_some()); } #[test] fn test_symbol_kinds() { use crate::resolve::names::DeclKind; let doc = Document::new(SAMPLE_STORYBOOK.to_string()); let alice = doc.name_table.resolve_name("Alice").unwrap(); assert_eq!(alice.kind, DeclKind::Character); let child = doc.name_table.resolve_name("Child").unwrap(); assert_eq!(child.kind, DeclKind::Template); let growing = doc.name_table.resolve_name("Growing").unwrap(); assert_eq!(growing.kind, DeclKind::LifeArc); } #[test] fn test_word_at_offset() { let doc = Document::new("character Alice {}".to_string()); // Test finding "character" keyword let word = doc.word_at_offset(5); assert_eq!(word, Some("character".to_string())); // Test finding "Alice" identifier let word = doc.word_at_offset(12); assert_eq!(word, Some("Alice".to_string())); // Test whitespace returns None let word = doc.word_at_offset(9); assert_eq!(word, None); } } #[cfg(test)] mod position_tests { use crate::position::PositionTracker; #[test] fn test_position_tracking_single_line() { let mut tracker = PositionTracker::new("hello world"); assert_eq!(tracker.offset_to_position(0), (0, 0)); assert_eq!(tracker.offset_to_position(6), (0, 6)); assert_eq!(tracker.offset_to_position(11), (0, 11)); } #[test] fn test_position_tracking_multiline() { let mut tracker = PositionTracker::new("line 1\nline 2\nline 3"); // Start of first line assert_eq!(tracker.offset_to_position(0), (0, 0)); // Start of second line (after \n at offset 6) assert_eq!(tracker.offset_to_position(7), (1, 0)); // Start of third line (after \n at offset 13) assert_eq!(tracker.offset_to_position(14), (2, 0)); // Middle of second line assert_eq!(tracker.offset_to_position(10), (1, 3)); } #[test] fn test_line_count() { let tracker = PositionTracker::new("line 1\nline 2\nline 3"); assert_eq!(tracker.line_count(), 3); } #[test] fn test_line_offset() { let tracker = PositionTracker::new("line 1\nline 2\nline 3"); assert_eq!(tracker.line_offset(0), Some(0)); assert_eq!(tracker.line_offset(1), Some(7)); assert_eq!(tracker.line_offset(2), Some(14)); assert_eq!(tracker.line_offset(3), None); } } #[cfg(test)] mod hover_tests { use super::*; #[test] fn test_hover_keywords() { // Test character keyword let hover = hover::get_hover_info("character Alice {}", 0, 5); assert!(hover.is_some()); let hover = hover.unwrap(); if let HoverContents::Markup(content) = hover.contents { assert!(content.value.contains("character")); assert!(content.value.contains("Defines a character entity")); } // Test template keyword let hover = hover::get_hover_info("template Child {}", 0, 2); assert!(hover.is_some()); // Test life_arc keyword let hover = hover::get_hover_info("life_arc Growing {}", 0, 5); assert!(hover.is_some()); } #[test] fn test_hover_non_keyword() { let hover = hover::get_hover_info("character Alice {}", 0, 12); assert!(hover.is_none()); } #[test] fn test_hover_invalid_position() { let hover = hover::get_hover_info("character Alice {}", 0, 100); assert!(hover.is_none()); } } #[cfg(test)] mod completion_tests { use super::*; #[test] fn test_keyword_completions() { let doc = Document::new(SAMPLE_STORYBOOK.to_string()); let params = CompletionParams { text_document_position: TextDocumentPositionParams { text_document: TextDocumentIdentifier { uri: Url::parse("file:///test.sb").unwrap(), }, position: Position { line: 0, character: 0, }, }, work_done_progress_params: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), context: None, }; let result = completion::get_completions(&doc, ¶ms); assert!(result.is_some()); if let Some(CompletionResponse::Array(items)) = result { // Should have keyword completions assert!(items.iter().any(|item| item.label == "character")); assert!(items.iter().any(|item| item.label == "template")); assert!(items.iter().any(|item| item.label == "life_arc")); // Should have entity completions from document assert!(items.iter().any(|item| item.label == "Alice")); assert!(items.iter().any(|item| item.label == "Bob")); } } #[test] fn test_completion_includes_snippets() { let doc = Document::new("".to_string()); let params = CompletionParams { text_document_position: TextDocumentPositionParams { text_document: TextDocumentIdentifier { uri: Url::parse("file:///test.sb").unwrap(), }, position: Position::default(), }, work_done_progress_params: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), context: None, }; let result = completion::get_completions(&doc, ¶ms); if let Some(CompletionResponse::Array(items)) = result { // Check that character completion has a snippet let character_item = items.iter().find(|item| item.label == "character"); assert!(character_item.is_some()); let character_item = character_item.unwrap(); assert!(character_item.insert_text.is_some()); assert_eq!( character_item.insert_text_format, Some(InsertTextFormat::SNIPPET) ); } } } #[cfg(test)] mod formatting_tests { use super::*; #[test] fn test_basic_formatting() { let doc = Document::new("character Alice{age:7}".to_string()); let options = FormattingOptions { tab_size: 4, insert_spaces: true, ..Default::default() }; let result = formatting::format_document(&doc, &options); assert!(result.is_some()); let edits = result.unwrap(); assert_eq!(edits.len(), 1); let formatted = &edits[0].new_text; eprintln!("Formatted output:\n{}", formatted); assert!(formatted.contains("character Alice {") || formatted.contains("character Alice{")); assert!(formatted.contains("age: 7") || formatted.contains("age:7")); } #[test] fn test_formatting_indentation() { let doc = Document::new("character Alice {\nage: 7\n}".to_string()); let options = FormattingOptions { tab_size: 4, insert_spaces: true, ..Default::default() }; let result = formatting::format_document(&doc, &options); assert!(result.is_some()); let formatted = &result.unwrap()[0].new_text; // Check that age is indented with 4 spaces assert!(formatted.contains(" age: 7")); } #[test] fn test_formatting_preserves_prose() { let doc = Document::new( "character Alice {\n---backstory\nSome irregular spacing\n---\n}".to_string(), ); let options = FormattingOptions::default(); let result = formatting::format_document(&doc, &options); let formatted = &result.unwrap()[0].new_text; // Prose content should be preserved exactly assert!(formatted.contains("Some irregular spacing")); } #[test] fn test_no_formatting_needed() { let already_formatted = "character Alice {\n age: 7\n}\n"; let doc = Document::new(already_formatted.to_string()); let options = FormattingOptions::default(); let result = formatting::format_document(&doc, &options); // Should return None if no changes needed assert!(result.is_none()); } } #[cfg(test)] mod symbols_tests { use super::*; #[test] fn test_extract_symbols_from_ast() { let doc = Document::new(SAMPLE_STORYBOOK.to_string()); let ast = doc.ast.as_ref().unwrap(); let mut positions = doc.positions.clone(); let symbols = symbols::extract_symbols_from_ast(ast, &mut positions); // Should have top-level declarations assert!(symbols.iter().any(|s| s.name == "Alice")); assert!(symbols.iter().any(|s| s.name == "Child")); assert!(symbols.iter().any(|s| s.name == "Bob")); assert!(symbols.iter().any(|s| s.name == "Growing")); assert!(symbols.iter().any(|s| s.name == "DailyRoutine")); assert!(symbols.iter().any(|s| s.name == "Human")); assert!(symbols.iter().any(|s| s.name == "Friendship")); } #[test] fn test_symbol_hierarchy() { let doc = Document::new(SAMPLE_STORYBOOK.to_string()); let ast = doc.ast.as_ref().unwrap(); let mut positions = doc.positions.clone(); let symbols = symbols::extract_symbols_from_ast(ast, &mut positions); // Alice should have children (fields) let alice = symbols.iter().find(|s| s.name == "Alice").unwrap(); assert!(alice.children.is_some()); let children = alice.children.as_ref().unwrap(); assert!(children.iter().any(|c| c.name == "age")); assert!(children.iter().any(|c| c.name == "backstory")); } #[test] fn test_symbol_kinds() { let doc = Document::new(SAMPLE_STORYBOOK.to_string()); let ast = doc.ast.as_ref().unwrap(); let mut positions = doc.positions.clone(); let symbols = symbols::extract_symbols_from_ast(ast, &mut positions); let alice = symbols.iter().find(|s| s.name == "Alice").unwrap(); assert_eq!(alice.kind, SymbolKind::CLASS); let child = symbols.iter().find(|s| s.name == "Child").unwrap(); assert_eq!(child.kind, SymbolKind::INTERFACE); } #[test] fn test_life_arc_states() { let doc = Document::new(SAMPLE_STORYBOOK.to_string()); let ast = doc.ast.as_ref().unwrap(); let mut positions = doc.positions.clone(); let symbols = symbols::extract_symbols_from_ast(ast, &mut positions); let growing = symbols.iter().find(|s| s.name == "Growing").unwrap(); assert!(growing.children.is_some()); let states = growing.children.as_ref().unwrap(); assert!(states.iter().any(|s| s.name == "child")); assert!(states.iter().any(|s| s.name == "teen")); assert!(states.iter().any(|s| s.name == "adult")); } } #[cfg(test)] mod definition_tests { use super::*; #[test] fn test_goto_definition_character() { let mut doc = Document::new(SAMPLE_STORYBOOK.to_string()); // Find position of "Alice" in "character Alice" let alice_offset = doc.text.find("character Alice").unwrap() + "character ".len(); let (line, col) = doc.positions.offset_to_position(alice_offset); let params = GotoDefinitionParams { text_document_position_params: TextDocumentPositionParams { text_document: TextDocumentIdentifier { uri: Url::parse("file:///test.sb").unwrap(), }, position: Position { line: line as u32, character: col as u32, }, }, work_done_progress_params: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), }; let uri = Url::parse("file:///test.sb").unwrap(); let result = definition::get_definition(&doc, ¶ms, &uri); assert!(result.is_some()); } #[test] fn test_goto_definition_not_found() { let doc = Document::new("character Alice {}".to_string()); let params = GotoDefinitionParams { text_document_position_params: TextDocumentPositionParams { text_document: TextDocumentIdentifier { uri: Url::parse("file:///test.sb").unwrap(), }, position: Position { line: 0, character: 0, // On "character" keyword, not a symbol }, }, work_done_progress_params: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), }; let uri = Url::parse("file:///test.sb").unwrap(); let result = definition::get_definition(&doc, ¶ms, &uri); assert!(result.is_none()); } } #[cfg(test)] mod references_tests { use super::*; #[test] fn test_find_references() { let source = "character Alice {}\ncharacter Bob { friend: Alice }"; let mut doc = Document::new(source.to_string()); // Find position of first "Alice" let alice_offset = source.find("Alice").unwrap(); let (line, col) = doc.positions.offset_to_position(alice_offset); let params = ReferenceParams { text_document_position: TextDocumentPositionParams { text_document: TextDocumentIdentifier { uri: Url::parse("file:///test.sb").unwrap(), }, position: Position { line: line as u32, character: col as u32, }, }, work_done_progress_params: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), context: ReferenceContext { include_declaration: true, }, }; let uri = Url::parse("file:///test.sb").unwrap(); let result = references::find_references(&doc, ¶ms, &uri); assert!(result.is_some()); let locations = result.unwrap(); // Should find both occurrences of "Alice" assert_eq!(locations.len(), 2); } #[test] fn test_find_references_word_boundaries() { let source = "character Alice {}\ncharacter Alicia {}"; let mut doc = Document::new(source.to_string()); let alice_offset = source.find("Alice").unwrap(); let (line, col) = doc.positions.offset_to_position(alice_offset); let params = ReferenceParams { text_document_position: TextDocumentPositionParams { text_document: TextDocumentIdentifier { uri: Url::parse("file:///test.sb").unwrap(), }, position: Position { line: line as u32, character: col as u32, }, }, work_done_progress_params: WorkDoneProgressParams::default(), partial_result_params: PartialResultParams::default(), context: ReferenceContext { include_declaration: true, }, }; let uri = Url::parse("file:///test.sb").unwrap(); let result = references::find_references(&doc, ¶ms, &uri); let locations = result.unwrap(); // Should only find "Alice", not "Alicia" assert_eq!(locations.len(), 1); } } #[cfg(test)] mod integration_tests { use super::*; #[test] fn test_full_workflow() { // Create a document let mut doc = Document::new(SAMPLE_STORYBOOK.to_string()); // Verify parsing worked assert!(doc.ast.is_some()); assert!(doc.parse_errors.is_empty()); // Verify symbols were extracted assert!(doc.name_table.all_entries().count() > 0); // Test updating document doc.update("character NewChar {}".to_string()); assert!(doc.name_table.resolve_name("NewChar").is_some()); // Verify new AST was parsed assert!(doc.ast.is_some()); } #[test] fn test_error_recovery() { let invalid = "character { invalid }"; let doc = Document::new(invalid.to_string()); // Should handle errors gracefully assert!(doc.ast.is_none()); assert!(!doc.parse_errors.is_empty()); // Symbols should be empty for invalid document assert_eq!(doc.name_table.all_entries().count(), 0); } }