use serde::{Deserialize, Serialize};
use strop_core::id::BufferRevision;
use strop_core::{Buffer, Range, Replacement};
use super::model::{self, Document, Event, Geometry, Model};
use crate::editor::transact::ChangeSet;
use crate::editor::Editor;
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Input {
start: u16,
end: u16,
text: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Step {
edits: Vec<Input>,
stale: bool,
readonly: bool,
}
fn document(editor: &Editor) -> Document {
Document {
id: editor.current(),
revision: editor.buf().revision(),
text: editor.buf().text().to_string(),
}
}
fn run(recipe: &[Step]) -> Result<(), String> {
let mut editor = Editor::new_in(Buffer::from_text("aĆ©\r\nšz\n"), "/recorded".into());
let mut model = Model::default();
model.step(Event::Open(document(&editor)))?;
let mut expected = editor.buf().text().to_string();
for (index, step) in recipe.iter().enumerate() {
let before = document(&editor);
let history_before =
serde_json::to_value(editor.buf().history()).map_err(|error| error.to_string())?;
editor.buf_mut().readonly = step.readonly;
let bound = expected.len() + 1;
let edits: Vec<_> = step
.edits
.iter()
.map(|input| model::Edit {
start: usize::from(input.start) % bound,
end: usize::from(input.end) % bound,
text: ["", "x", "Ć©", "\r\n", "š"][usize::from(input.text) % 5].into(),
})
.collect();
let prediction = model::apply_text(&expected, Geometry::PreEdit, &edits);
let revision = before.revision;
let base = if step.stale {
BufferRevision::new(
revision
.get()
.checked_add(1)
.ok_or("recipe revision exhausted")?,
)
} else {
revision
};
let changes = ChangeSet {
edits: edits
.iter()
.map(|edit| {
Replacement::new(
Range {
start: (edit.start % bound).into(),
end: (edit.end % bound).into(),
shape: strop_core::MotionShape::Characterwise { inclusive: false },
},
edit.text.clone(),
)
})
.collect(),
undo_open: false,
};
let result = editor.apply(editor.current(), base, changes);
let should_accept = prediction.is_ok() && !step.stale && !step.readonly;
if result.is_ok() != should_accept {
return Err(format!(
"step {index}: acceptance disagrees with independent geometry (editor {}, oracle {})",
result.is_ok(),
should_accept
));
}
let after = document(&editor);
if should_accept {
expected = prediction.expect("accepted geometry");
if after.text != expected {
return Err(format!("step {index}: wrong bytes"));
}
model.step(Event::Publish {
document: before.id,
base: revision,
next: after.revision,
geometry: Geometry::PreEdit,
edits,
text: after.text.clone(),
})?;
} else {
model.step(Event::Refused {
before,
after: after.clone(),
history_before,
history_after: serde_json::to_value(editor.buf().history())
.map_err(|error| error.to_string())?,
})?;
}
model.step(Event::Observe {
documents: vec![after],
panes: editor.panes.iter().map(|pane| pane.doc).collect(),
})?;
}
Ok(())
}
fn minimize(mut recipe: Vec<Step>) -> Vec<Step> {
let mut width = recipe.len().max(1);
while width > 0 {
let mut start = 0;
while start < recipe.len() {
let end = (start + width).min(recipe.len());
let mut candidate = recipe.clone();
candidate.drain(start..end);
if run(&candidate).is_err() {
recipe = candidate;
} else {
start += width;
}
}
width /= 2;
}
recipe
}
fn next(state: &mut u64) -> u64 {
*state = state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
*state
}
#[test]
fn generated_publications_match_text_revision_and_refusal_model() {
let mut seed = 0x7374_726f_705f_7231_u64;
for _ in 0..64 {
let mut recipe = Vec::new();
for _ in 0..32 {
let word = next(&mut seed);
let count = 1 + (word & 1) as usize;
let mut edits = Vec::new();
for _ in 0..count {
let word = next(&mut seed);
edits.push(Input {
start: word as u16,
end: (word >> 16) as u16,
text: (word >> 32) as u8,
});
}
recipe.push(Step {
edits,
stale: word & 0x1f == 0,
readonly: word & 0x3f == 1,
});
}
if let Err(error) = run(&recipe) {
let smallest = minimize(recipe);
panic!(
"{error}; reproduce with run(serde_json::from_str::<Vec<Step>>(JSON).unwrap()): {}",
serde_json::to_string(&smallest).unwrap()
);
}
}
}
#[test]
fn adjacent_and_overlapping_regressions_exercise_real_gateway() {
let adjacent = Step {
edits: vec![
Input {
start: 0,
end: 1,
text: 1,
},
Input {
start: 1,
end: 3,
text: 2,
},
],
stale: false,
readonly: false,
};
let overlap = Step {
edits: vec![
Input {
start: 0,
end: 3,
text: 1,
},
Input {
start: 1,
end: 3,
text: 2,
},
],
stale: false,
readonly: false,
};
run(&[adjacent, overlap]).unwrap();
}