use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use increparse::{
CancelToken, Engine, Outcome, Pass, Schedule, SerialExecutor, Session, Span, Status,
};
#[derive(Clone, Debug, PartialEq, Eq)]
enum Ctx {
File,
Left,
Right { head: char },
}
#[derive(Clone)]
struct SplitAtFour {
calls: Arc<AtomicUsize>,
}
impl Pass for SplitAtFour {
type Ctx = Ctx;
fn parse(&self, source: &str, span: Span, ctx: &Ctx) -> Outcome<Ctx> {
self.calls.fetch_add(1, Ordering::SeqCst);
if !matches!(ctx, Ctx::File) {
return Outcome::Failed;
}
let split = (span.start + 4).min(span.end);
let head = source[split..].chars().next().unwrap_or('?');
Outcome::Expand(vec![
(Span::new(span.start, split, span.rev), Ctx::Left),
(Span::new(split, span.end, span.rev), Ctx::Right { head }),
])
}
}
#[derive(Clone)]
struct Settle {
calls: Arc<AtomicUsize>,
}
impl Pass for Settle {
type Ctx = Ctx;
fn parse(&self, _source: &str, _span: Span, ctx: &Ctx) -> Outcome<Ctx> {
self.calls.fetch_add(1, Ordering::SeqCst);
match ctx {
Ctx::File => Outcome::Failed,
_ => Outcome::Done,
}
}
}
#[derive(Clone)]
struct NoBang {
calls: Arc<AtomicUsize>,
}
impl Pass for NoBang {
type Ctx = Ctx;
fn parse(&self, source: &str, span: Span, ctx: &Ctx) -> Outcome<Ctx> {
self.calls.fetch_add(1, Ordering::SeqCst);
if matches!(ctx, Ctx::File) {
return Outcome::Failed;
}
if source[span.to_range()].contains('!') {
Outcome::Failed
} else {
Outcome::Done
}
}
}
fn make_session(source: &str) -> (Session<Ctx>, Arc<AtomicUsize>, Arc<AtomicUsize>) {
let split_calls = Arc::new(AtomicUsize::new(0));
let settle_calls = Arc::new(AtomicUsize::new(0));
let mut schedule = Schedule::new();
schedule.push(SplitAtFour {
calls: split_calls.clone(),
});
schedule.push(Settle {
calls: settle_calls.clone(),
});
let engine = Engine::new(schedule);
let mut session: Session<Ctx> = Session::new(0, Span::new(0, source.len(), 0), Ctx::File);
let report = session.run(&engine, source, &SerialExecutor, &CancelToken::new());
assert!(report.reached_fixpoint);
(session, split_calls, settle_calls)
}
#[test]
fn untouched_sibling_is_reused_with_same_identity() {
let source = "abcdefghij";
let (mut session, split_calls, settle_calls) = make_session(source);
let root = session.tree().root();
let left = session.tree().children(root)[0];
let right = session.tree().children(root)[1];
assert_eq!(split_calls.load(Ordering::SeqCst), 1);
assert_eq!(settle_calls.load(Ordering::SeqCst), 2);
let edited = "abcdefgij";
session.edit(increparse::Edit::replace(7, 8, 8));
assert_eq!(session.revision(), 1);
let engine = Engine::new({
let mut s = Schedule::new();
s.push(SplitAtFour {
calls: split_calls.clone(),
});
s.push(Settle {
calls: settle_calls.clone(),
});
s
});
let report = session.run(&engine, edited, &SerialExecutor, &CancelToken::new());
assert!(report.reached_fixpoint);
let kids = session.tree().children(root);
assert_eq!(kids, &[left, right]);
assert_eq!(session.tree().status(left), Status::Done);
assert_eq!(session.tree().status(right), Status::Done);
assert_eq!(split_calls.load(Ordering::SeqCst), 2);
assert_eq!(settle_calls.load(Ordering::SeqCst), 3);
assert_eq!(session.tree().span(right).to_range(), 4..10);
assert_eq!(session.tree().ctx(right), &Ctx::Right { head: 'e' });
}
#[test]
fn insert_at_end_reuses_left_and_recreates_shifted_right() {
let source = "abcdefghij";
let (mut session, split_calls, settle_calls) = make_session(source);
let root = session.tree().root();
let left = session.tree().children(root)[0];
let right = session.tree().children(root)[1];
let edited = "abcdefghijXYZ";
session.edit(increparse::Edit::insert(10, 3));
session.run(
&Engine::new({
let mut s = Schedule::new();
s.push(SplitAtFour {
calls: split_calls.clone(),
});
s.push(Settle {
calls: settle_calls.clone(),
});
s
}),
edited,
&SerialExecutor,
&CancelToken::new(),
);
let kids = session.tree().children(root);
assert_eq!(kids[0], left);
assert_eq!(session.tree().status(left), Status::Done);
assert_eq!(session.tree().span(left).to_range(), 0..4);
assert_eq!(kids[1], right);
assert_eq!(session.tree().span(kids[1]).to_range(), 4..13);
assert_eq!(session.tree().status(kids[1]), Status::Done);
assert_eq!(session.tree().status_counts().total(), 3);
assert!(session.tree().nodes().any(|id| id == right));
assert_eq!(settle_calls.load(Ordering::SeqCst), 3);
}
#[test]
fn context_change_recreates_node_and_detaches_stale_subtree() {
let source = "abcdefghij";
let (mut session, split_calls, settle_calls) = make_session(source);
let root = session.tree().root();
let left = session.tree().children(root)[0];
let right = session.tree().children(root)[1];
let edited = "abcdxfghi";
session.edit(increparse::Edit::replace(4, 5, 5));
session.run(
&Engine::new({
let mut s = Schedule::new();
s.push(SplitAtFour {
calls: split_calls.clone(),
});
s.push(Settle {
calls: settle_calls.clone(),
});
s
}),
edited,
&SerialExecutor,
&CancelToken::new(),
);
let kids = session.tree().children(root);
assert_eq!(kids[0], left);
assert_ne!(kids[1], right);
assert_eq!(session.tree().ctx(kids[1]), &Ctx::Right { head: 'x' });
assert_eq!(session.tree().status(kids[1]), Status::Done);
assert_eq!(session.tree().status_counts().total(), 3);
assert_eq!(session.tree().len(), 3);
assert!(!session.tree().nodes().any(|id| id == right));
assert_eq!(settle_calls.load(Ordering::SeqCst), 3);
}
#[test]
fn edit_heals_previously_failed_region() {
let source = "abcd!efghi";
let nobang_calls = Arc::new(AtomicUsize::new(0));
let split_calls = Arc::new(AtomicUsize::new(0));
let mut schedule = Schedule::new();
schedule.push(SplitAtFour {
calls: split_calls.clone(),
});
schedule.push(NoBang {
calls: nobang_calls.clone(),
});
let engine = Engine::new(schedule);
let mut session: Session<Ctx> = Session::new(0, Span::new(0, source.len(), 0), Ctx::File);
let report = session.run(&engine, source, &SerialExecutor, &CancelToken::new());
assert!(report.reached_fixpoint);
let root = session.tree().root();
let left = session.tree().children(root)[0];
let right = session.tree().children(root)[1];
assert_eq!(session.tree().status(left), Status::Done);
assert_eq!(session.tree().status(right), Status::Failed);
assert_eq!(session.tree().attempts(right), 1);
let edited = "abcd.efghi";
session.edit(increparse::Edit::replace(4, 5, 5));
let report = session.run(&engine, edited, &SerialExecutor, &CancelToken::new());
assert!(report.reached_fixpoint);
let kids = session.tree().children(root);
assert_eq!(kids[0], left);
assert_ne!(kids[1], right);
assert!(!session.tree().nodes().any(|id| id == right));
assert_eq!(session.tree().status(kids[1]), Status::Done);
assert_eq!(session.tree().attempts(kids[1]), 0);
assert_eq!(nobang_calls.load(Ordering::SeqCst), 3);
}
#[test]
fn session_revision_tracks_edits() {
let source = "abcdefghij";
let (mut session, _split, _settle) = make_session(source);
assert_eq!(session.revision(), 0);
session.edit(increparse::Edit::insert(5, 2));
assert_eq!(session.revision(), 1);
session.edit(increparse::Edit::delete(2, 3));
assert_eq!(session.revision(), 2);
assert_eq!(session.tree().source_rev(), 2);
}
#[test]
fn edit_keeps_engine_contract_violations_detected() {
struct EscapeOnFile;
impl Pass for EscapeOnFile {
type Ctx = Ctx;
fn parse(&self, _source: &str, span: Span, ctx: &Ctx) -> Outcome<Ctx> {
if !matches!(ctx, Ctx::File) {
return Outcome::Failed;
}
Outcome::Expand(vec![(
Span::new(span.start, span.end + 1, span.rev),
Ctx::Left,
)])
}
}
let mut schedule = Schedule::new();
schedule.push(EscapeOnFile);
let engine = Engine::new(schedule);
let mut session: Session<Ctx> = Session::new(0, Span::new(0, 10, 0), Ctx::File);
session.run(&engine, "abcdefghij", &SerialExecutor, &CancelToken::new());
assert_eq!(session.tree().status(session.tree().root()), Status::Failed);
session.edit(increparse::Edit::insert(5, 1));
let report = session.run(&engine, "abcdefghijk", &SerialExecutor, &CancelToken::new());
assert!(report.reached_fixpoint);
assert_eq!(session.tree().status(session.tree().root()), Status::Failed);
assert_eq!(report.nodes_failed, 1);
}