use super::incremental_document::IncrementalDocument;
use super::incremental_edit::{IncrementalEdit, IncrementalEditSet};
use perl_parser_core::{error::ParseResult, parser::Parser};
#[test]
fn overlapping_batch_edits_fall_back_safely() -> ParseResult<()> {
let source = "my $x = 10;\n".to_string();
let mut document = IncrementalDocument::new(source.clone())?;
let mut edits = IncrementalEditSet::new();
edits.add(IncrementalEdit::new(8, 10, "20".to_string()));
edits.add(IncrementalEdit::new(9, 10, "5".to_string()));
document.apply_edits(&edits)?;
assert_eq!(document.source, "my $x = 20;\n");
assert_eq!(document.metrics.nodes_reused, 0);
assert!(document.source.starts_with("my $x"));
Ok(())
}
#[test]
fn backwards_range_batch_edit_is_rejected() -> ParseResult<()> {
let source = "my $x = 10;\n".to_string();
let mut document = IncrementalDocument::new(source.clone())?;
let mut edits = IncrementalEditSet::new();
edits.add(IncrementalEdit::new(9, 7, "5".to_string()));
document.apply_edits(&edits)?;
assert_eq!(document.source, source);
assert_eq!(document.metrics.nodes_reused, 0);
Ok(())
}
#[test]
fn mid_codepoint_edit_attempt_falls_back() -> ParseResult<()> {
let source = "my $x = \"é\";\n".to_string();
let mut document = IncrementalDocument::new(source.clone())?;
let accent_start =
source.find('é').ok_or_else(|| perl_parser_core::error::ParseError::SyntaxError {
message: "test source should contain 'é'".to_string(),
location: 0,
})?;
let mut edits = IncrementalEditSet::new();
edits.add(IncrementalEdit::new(accent_start + 1, accent_start + 1, "x".to_string()));
document.apply_edits(&edits)?;
assert_eq!(document.source, source);
assert_eq!(document.metrics.nodes_reused, 0);
Ok(())
}
#[test]
fn batch_with_one_unmappable_edit_uses_fallback() -> ParseResult<()> {
let source = "my $x = \"é\";\n".to_string();
let mut document = IncrementalDocument::new(source.clone())?;
let accent_start =
source.find('é').ok_or_else(|| perl_parser_core::error::ParseError::SyntaxError {
message: "test source should contain 'é'".to_string(),
location: 0,
})?;
let mut edits = IncrementalEditSet::new();
edits.add(IncrementalEdit::new(4, 6, "$value".to_string()));
edits.add(IncrementalEdit::new(accent_start + 1, accent_start + 1, "x".to_string()));
let expected = edits.apply_to_string(&source);
document.apply_edits(&edits)?;
assert_eq!(document.source, expected);
assert!(document.source.contains("$value"));
assert_eq!(document.metrics.nodes_reused, 0);
Ok(())
}
#[test]
fn supported_batch_edits_match_fresh_parse() -> ParseResult<()> {
let source = "my $x = 10;\nmy $y = 20;\n".to_string();
let mut document = IncrementalDocument::new(source)?;
let mut edits = IncrementalEditSet::new();
edits.add(IncrementalEdit::new(8, 10, "11".to_string()));
edits.add(IncrementalEdit::new(20, 22, "21".to_string()));
document.apply_edits(&edits)?;
let mut parser = Parser::new(&document.source);
let parsed_fresh = parser.parse()?;
assert_eq!(
*document.root, parsed_fresh,
"incremental batch result must match a fresh parse of the same source"
);
Ok(())
}
#[test]
fn empty_edit_set_is_a_noop() -> ParseResult<()> {
let source = "my $x = 42;\n".to_string();
let mut document = IncrementalDocument::new(source.clone())?;
let root_before = (*document.root).clone();
let edits = IncrementalEditSet::new();
document.apply_edits(&edits)?;
assert_eq!(document.source, source, "source must be unchanged for empty edit set");
assert_eq!(*document.root, root_before, "tree must be unchanged for empty edit set");
Ok(())
}
#[test]
fn adjacent_non_overlapping_edits_both_apply() -> ParseResult<()> {
let source = "my $x = 10; my $y = 20;\n".to_string();
let mut document = IncrementalDocument::new(source.clone())?;
assert_eq!(&source[8..10], "10", "byte offset assumption for edit A");
assert_eq!(&source[20..22], "20", "byte offset assumption for edit B");
let mut edits = IncrementalEditSet::new();
edits.add(IncrementalEdit::new(8, 10, "99".to_string()));
edits.add(IncrementalEdit::new(20, 22, "88".to_string()));
document.apply_edits(&edits)?;
assert!(
document.source.contains("99"),
"edit A ($x value) must be applied; got: {}",
document.source
);
assert!(
document.source.contains("88"),
"edit B ($y value) must be applied; got: {}",
document.source
);
assert_eq!(document.source, "my $x = 99; my $y = 88;\n");
let mut parser = Parser::new(&document.source);
let parsed_fresh = parser.parse()?;
assert_eq!(*document.root, parsed_fresh, "incremental batch must match fresh parse");
Ok(())
}
#[test]
fn adjacent_touching_ranges_are_not_rejected() -> ParseResult<()> {
let source = "abcdef".to_string();
let mut document = IncrementalDocument::new(source)?;
let mut edits = IncrementalEditSet::new();
edits.add(IncrementalEdit::new(0, 3, "XY".to_string()));
edits.add(IncrementalEdit::new(3, 6, "Z".to_string()));
document.apply_edits(&edits)?;
assert_eq!(document.source, "XYZ", "adjacent touching edits must both apply");
Ok(())
}
#[test]
fn whole_file_replacement_batch_takes_single_parse_path() -> ParseResult<()> {
let source = "my $x = 1;\n".to_string();
let mut document = IncrementalDocument::new(source)?;
let mut edits = IncrementalEditSet::new();
let new_content = "my $y = 99;\n".to_string();
edits.add(IncrementalEdit::new(0, 11, new_content.trim_end_matches('\n').to_string()));
document.apply_edits(&edits)?;
assert!(
document.source.contains("99"),
"whole-file replacement must produce updated source; got: {}",
document.source
);
let mut parser = Parser::new(&document.source);
let parsed_fresh = parser.parse()?;
assert_eq!(*document.root, parsed_fresh, "whole-file replacement must match fresh parse");
Ok(())
}
#[test]
fn same_start_edits_apply_deterministically() -> ParseResult<()> {
let source = "abcdefgh".to_string();
let mut document = IncrementalDocument::new(source.clone())?;
let mut edits = IncrementalEditSet::new();
edits.add(IncrementalEdit::new(2, 6, "X".to_string())); edits.add(IncrementalEdit::new(2, 4, "Y".to_string()));
let expected = edits.apply_to_string(&source);
document.apply_edits(&edits)?;
assert_eq!(
document.source, expected,
"same-start overlapping edits: fallback result must be deterministic"
);
Ok(())
}