release: Storybook v0.2.0 - Major syntax and features update
BREAKING CHANGES: - Relationship syntax now requires blocks for all participants - Removed self/other perspective blocks from relationships - Replaced 'guard' keyword with 'if' for behavior tree decorators Language Features: - Add tree-sitter grammar with improved if/condition disambiguation - Add comprehensive tutorial and reference documentation - Add SBIR v0.2.0 binary format specification - Add resource linking system for behaviors and schedules - Add year-long schedule patterns (day, season, recurrence) - Add behavior tree enhancements (named nodes, decorators) Documentation: - Complete tutorial series (9 chapters) with baker family examples - Complete reference documentation for all language features - SBIR v0.2.0 specification with binary format details - Added locations and institutions documentation Examples: - Convert all examples to baker family scenario - Add comprehensive working examples Tooling: - Zed extension with LSP integration - Tree-sitter grammar for syntax highlighting - Build scripts and development tools Version Updates: - Main package: 0.1.0 → 0.2.0 - Tree-sitter grammar: 0.1.0 → 0.2.0 - Zed extension: 0.1.0 → 0.2.0 - Storybook editor: 0.1.0 → 0.2.0
This commit is contained in:
623
src/lsp/tests.rs
Normal file
623
src/lsp/tests.rs
Normal file
@@ -0,0 +1,623 @@
|
||||
//! 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
|
||||
}
|
||||
|
||||
enum Mood {
|
||||
Happy,
|
||||
Sad,
|
||||
Angry
|
||||
}
|
||||
|
||||
character Alice: Human {
|
||||
age: 7
|
||||
mood: Happy
|
||||
---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("Mood").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 == "Mood"));
|
||||
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);
|
||||
|
||||
let mood = symbols.iter().find(|s| s.name == "Mood").unwrap();
|
||||
assert_eq!(mood.kind, SymbolKind::ENUM);
|
||||
}
|
||||
|
||||
#[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(&mut doc, ¶ms, &uri);
|
||||
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_goto_definition_not_found() {
|
||||
let mut 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(&mut 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(&mut 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(&mut 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user