mod util;
use ochrepad_core::position::{byte_to_visual_col, next_grapheme_boundary, visual_col_to_byte};
use ochrepad_core::{Action, ByteOffset, Document, Motion, SelectionSet};
use proptest::prelude::*;
const PIECES: &[&str] = &[
"ß",
"İ",
"Dž",
"1",
"-2.5",
"a",
"b",
"Z",
"_",
".",
"(",
" ",
" ",
"\t",
"\n",
"\n",
"e\u{301}",
"漢",
"👨👩👧",
"👍🏽",
"9",
];
fn text() -> impl Strategy<Value = String> {
prop::collection::vec(prop::sample::select(PIECES), 0..60).prop_map(|v| v.concat())
}
fn motion() -> impl Strategy<Value = Motion> {
prop_oneof![
Just(Motion::Left),
Just(Motion::Right),
Just(Motion::WordLeft),
Just(Motion::WordRight),
Just(Motion::Home),
Just(Motion::LineStart),
Just(Motion::LineEnd),
Just(Motion::RowHome),
Just(Motion::RowEnd),
Just(Motion::DocStart),
Just(Motion::DocEnd),
Just(Motion::Up),
Just(Motion::Down),
(0usize..5).prop_map(Motion::PageUp),
(0usize..5).prop_map(Motion::PageDown),
]
}
fn short_text() -> impl Strategy<Value = String> {
prop::collection::vec(prop::sample::select(PIECES), 0..3).prop_map(|v| v.concat())
}
fn search_query() -> impl Strategy<Value = ochrepad_core::SearchQuery> {
let pattern = prop_oneof![
prop::sample::select(PIECES).prop_map(str::to_owned),
prop::sample::select(vec!["^", "$", r"\w+", r"(a)(b)?", "a*", r"\s", "."])
.prop_map(str::to_owned),
];
(pattern, any::<[bool; 5]>()).prop_map(|(pattern, b)| {
ochrepad_core::SearchQuery::new(
pattern,
ochrepad_core::SearchOptions {
regex: b[0],
match_case: b[1],
whole_word: b[2],
wrap: b[3],
in_selection: b[4],
},
)
})
}
fn action() -> impl Strategy<Value = Action> {
prop_oneof![
4 => prop::sample::select(PIECES).prop_map(|s| Action::InsertText(s.to_owned())),
1 => text().prop_map(Action::InsertText),
1 => Just(Action::InsertNewline),
2 => Just(Action::DeleteBackward),
1 => Just(Action::DeleteForward),
1 => Just(Action::DeleteWordBackward),
1 => Just(Action::DeleteWordForward),
5 => (motion(), any::<bool>()).prop_map(|(motion, extend)| Action::MoveCursor { motion, extend }),
1 => Just(Action::SelectAll),
1 => Just(Action::SelectWord),
1 => Just(Action::SelectLine),
1 => Just(Action::AddCursorAbove),
2 => Just(Action::AddCursorBelow),
1 => Just(Action::CollapseSelections),
1 => text().prop_map(Action::Paste),
1 => text().prop_map(Action::PasteBlock),
2 => prop::sample::select(vec![
ochrepad_core::CaseMode::Upper,
ochrepad_core::CaseMode::Lower,
ochrepad_core::CaseMode::Title,
ochrepad_core::CaseMode::Invert,
])
.prop_map(Action::ConvertCase),
1 => any::<[bool; 4]>().prop_map(|b| Action::SortLines(ochrepad_core::SortOptions {
descending: b[0],
case_insensitive: b[1],
numeric: b[2],
remove_duplicates: b[3],
})),
1 => prop::sample::select(vec![
Action::TrimTrailingWhitespace,
Action::DuplicateLine,
Action::MoveLinesUp,
Action::MoveLinesDown,
Action::JoinLines,
Action::RemoveEmptyLines { include_whitespace: false },
Action::RemoveEmptyLines { include_whitespace: true },
Action::RemoveDuplicateLines { consecutive_only: false },
Action::RemoveDuplicateLines { consecutive_only: true },
]),
2 => (search_query(), short_text()).prop_map(|(query, replacement)| Action::ReplaceAll { query, replacement }),
1 => (search_query(), short_text()).prop_map(|(query, replacement)| Action::Replace { query, replacement }),
1 => (search_query(), any::<bool>()).prop_map(|(query, backward)| Action::Find { query, backward }),
1 => search_query().prop_map(Action::SelectAllMatches),
2 => (0usize..12, 0usize..20, 0usize..12, 0usize..20).prop_map(|(al, ac, hl, hc)| {
Action::ColumnSelect {
anchor_line: al,
anchor_col: ochrepad_core::VisualCol(ac),
head_line: hl,
head_col: ochrepad_core::VisualCol(hc),
}
}),
2 => prop::sample::select(vec![
ochrepad_core::Direction::Up,
ochrepad_core::Direction::Down,
ochrepad_core::Direction::Left,
ochrepad_core::Direction::Right,
])
.prop_map(Action::ColumnExtend),
1 => prop::collection::vec((0usize..300, 0usize..300), 1..4).prop_map(|v| {
let ranges = v
.into_iter()
.map(|(a, h)| ochrepad_core::Selection::new(ByteOffset(a), ByteOffset(h)))
.collect();
Action::SetSelections(SelectionSet::new(ranges, 0))
}),
2 => Just(Action::Undo),
1 => Just(Action::Redo),
2 => prop_oneof![
Just(Action::Fold),
Just(Action::Unfold),
Just(Action::ToggleFold),
Just(Action::FoldAll),
Just(Action::UnfoldAll),
(1usize..4).prop_map(Action::FoldLevel),
(1usize..4).prop_map(Action::UnfoldLevel),
],
]
}
fn check(t: &util::T) -> Result<(), TestCaseError> {
let sels = t.sels();
let doc = t.buffer().doc();
sels.validate(doc)
.map_err(|e| TestCaseError::fail(format!("{e}\nstate: {}", t.show())))?;
let view = t.ed.view(t.view).unwrap();
let folds = view.folds();
for s in sels.ranges() {
prop_assert!(
!folds.is_hidden(doc.line_of(s.head)),
"hidden caret: {}",
t.show()
);
}
let rows = ochrepad_core::DisplayMap::new(t.buffer(), view, 4);
let padded = view.padding.is_some();
let visible: Vec<usize> = (0..doc.line_count())
.filter(|&l| padded || !folds.is_hidden(l))
.collect();
prop_assert_eq!(rows.visible_line_count(), visible.len());
let all: Vec<_> = rows.rows_from(rows.first_row(), usize::MAX).collect();
let mut shown: Vec<usize> = all.iter().map(|r| r.line).collect();
shown.dedup();
prop_assert_eq!(shown, visible);
for (i, &r) in all.iter().enumerate() {
let range = rows.row_range(r);
let blank = rows.is_filler(r);
if blank {
prop_assert!(range.is_empty());
} else {
prop_assert_eq!(rows.row_of(range.start), r);
}
if r.sub == 0 {
prop_assert_eq!(range.start, doc.line_start(r.line));
} else {
prop_assert_eq!(rows.row_range(all[i - 1]).end, range.start);
}
if !blank && rows.is_last_in_line(r) {
prop_assert_eq!(range.end, doc.line_end(r.line));
}
prop_assert!(rows.row_end(r) >= range.start && rows.row_end(r) <= range.end);
}
Ok(())
}
type Layout = (Option<usize>, Option<(Vec<usize>, usize)>);
fn layout() -> impl Strategy<Value = Layout> {
(
prop::option::of(10usize..16),
prop::option::weighted(0.3, (prop::collection::vec(0usize..3, 0..8), 0usize..3)),
)
}
fn setup_wrapped(initial: &str, (wrap, padding): Layout) -> util::T {
let mut t = util::setup(initial);
let v = t.ed.view_mut(t.view).unwrap();
v.wrap_cols = wrap;
v.padding = padding.map(|(before, after_last)| {
std::sync::Arc::new(ochrepad_core::Padding { before, after_last })
});
t
}
proptest! {
#![proptest_config(ProptestConfig { cases: 3000, ..ProptestConfig::default() })]
#[test]
fn undo_all_restores_original(
initial in text(),
actions in prop::collection::vec(action(), 0..40),
layout in layout(),
) {
let mut t = setup_wrapped(&initial, layout);
let mut first_edit_sels: Option<SelectionSet> = None;
for a in actions {
let before = t.sels();
let rev = t.buffer().revision();
let had_undo = t.buffer().history().can_undo();
let is_redo = a == Action::Redo;
let _ = t.ed.apply(t.view, a);
check(&t)?;
if !had_undo && !is_redo && t.buffer().revision() != rev {
first_edit_sels = Some(before);
}
}
let undid_something = t.buffer().history().can_undo();
while t.buffer().history().can_undo() {
t.apply(Action::Undo);
check(&t)?;
}
prop_assert_eq!(t.text(), initial);
prop_assert!(!t.buffer().is_dirty());
if let Some(s) = first_edit_sels.filter(|_| undid_something) {
prop_assert_eq!(t.sels(), s);
}
}
#[test]
fn undo_then_redo_is_identity(
initial in text(),
actions in prop::collection::vec(action(), 1..40),
layout in layout(),
) {
let mut t = setup_wrapped(&initial, layout);
let mut last_was_edit = false;
for a in actions {
let rev = t.buffer().revision();
let is_undo = a == Action::Undo;
let _ = t.ed.apply(t.view, a);
last_was_edit = !is_undo && t.buffer().revision() != rev;
}
if !t.buffer().history().can_undo() {
return Ok(());
}
let (text, sels, rev) = (t.text(), t.sels(), t.buffer().revision());
t.apply(Action::Undo).apply(Action::Redo);
check(&t)?;
prop_assert_eq!(t.text(), text);
prop_assert_eq!(t.buffer().revision(), rev);
if last_was_edit {
prop_assert_eq!(t.sels(), sels);
}
}
#[test]
fn line_edits_track_line_count(
initial in text(),
actions in prop::collection::vec(action(), 0..40),
layout in layout(),
) {
let mut t = setup_wrapped(&initial, layout);
let id = t.buffer().id();
for a in actions {
let before = t.buffer().doc().line_count() as isize;
let _ = t.ed.apply(t.view, a);
let edits = t.ed.buffer_mut(id).unwrap().take_line_edits();
if edits.iter().any(|e| e.is_everything()) {
continue;
}
let mut lines = before;
for e in &edits {
prop_assert!(e.start <= e.old_end && (e.old_end as isize) < lines);
lines += e.new_end as isize - e.old_end as isize;
}
prop_assert_eq!(lines, t.buffer().doc().line_count() as isize);
}
}
#[test]
fn column_selections_stay_inside_the_rectangle(
s in text(),
(al, ac, hl, hc) in (0usize..12, 0usize..20, 0usize..12, 0usize..20),
) {
let mut t = util::setup(&s);
t.apply(Action::ColumnSelect {
anchor_line: al,
anchor_col: ochrepad_core::VisualCol(ac),
head_line: hl,
head_col: ochrepad_core::VisualCol(hc),
});
check(&t)?;
let sels = t.sels();
let rect = sels.rect().cloned().expect("column selection keeps its rectangle");
let doc = t.buffer().doc();
prop_assert_eq!(sels.len(), rect.bottom() - rect.top() + 1);
for (i, sel) in sels.ranges().iter().enumerate() {
let line = rect.top() + i;
prop_assert_eq!(doc.line_of(sel.start()), line);
prop_assert_eq!(doc.line_of(sel.end()), line);
let end_col = byte_to_visual_col(doc, sel.end(), 4);
prop_assert!(end_col <= rect.right());
if !sel.is_empty() {
prop_assert!(byte_to_visual_col(doc, sel.start(), 4) >= rect.left());
}
}
}
#[test]
fn visual_col_round_trip(s in text(), tab_width in 1usize..9) {
let doc = Document::new(&s);
let mut p = ByteOffset(0);
loop {
let col = byte_to_visual_col(&doc, p, tab_width);
let back = visual_col_to_byte(&doc, doc.line_of(p), col, tab_width);
prop_assert_eq!(back, p, "col {:?} in {:?}", col, s);
if p == doc.len() {
break;
}
p = next_grapheme_boundary(&doc, p);
}
}
}
proptest! {
#![proptest_config(ProptestConfig { cases: 2000, ..ProptestConfig::default() })]
#[test]
fn diff_rows_cover_both_sides(a in text(), b in text()) {
use ochrepad_core::diff::{RowKind, diff_lines, diff_words};
let (left, right) = (Document::new(&a), Document::new(&b));
let d = diff_lines(&left, &right);
let ls: Vec<usize> = d.rows.iter().filter_map(|r| r.left).collect();
let rs: Vec<usize> = d.rows.iter().filter_map(|r| r.right).collect();
prop_assert_eq!(ls, (0..left.line_count()).collect::<Vec<_>>());
prop_assert_eq!(rs, (0..right.line_count()).collect::<Vec<_>>());
let mut in_hunk = vec![false; d.rows.len()];
for h in &d.hunks {
for i in h.clone() {
in_hunk[i] = true;
}
}
for (i, r) in d.rows.iter().enumerate() {
prop_assert_eq!(in_hunk[i], r.kind != RowKind::Equal);
if let (Some(l), Some(rr)) = (r.left, r.right) {
let (lt, rt) = (left.line_text(l), right.line_text(rr));
if r.kind == RowKind::Equal {
prop_assert_eq!(<, &rt);
} else {
let (wa, wb) = diff_words(<, &rt);
for w in &wa {
prop_assert!(lt.is_char_boundary(w.start) && lt.is_char_boundary(w.end));
}
for w in &wb {
prop_assert!(rt.is_char_boundary(w.start) && rt.is_char_boundary(w.end));
}
}
}
}
}
}