From 2b959e5a68841bcf75db79f1323cfd450fb542d6 Mon Sep 17 00:00:00 2001 From: Matthew Gordon Date: Sat, 9 Nov 2024 11:35:27 -0400 Subject: [PATCH] Do some automated random testing of Rope --- core/Cargo.toml | 1 + core/src/rope/mod.rs | 3 +- core/src/rope/tests/command_list.rs | 69 +++++++++++++++++++++ core/src/rope/tests/mod.rs | 95 +++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 core/src/rope/tests/command_list.rs diff --git a/core/Cargo.toml b/core/Cargo.toml index 9bcd248..28c13d4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -5,3 +5,4 @@ edition = "2021" [dev-dependencies] ntest = "0.9.3" +rand = {version="0.8.5", features=["small_rng"]} diff --git a/core/src/rope/mod.rs b/core/src/rope/mod.rs index 6b9374b..9c69e96 100644 --- a/core/src/rope/mod.rs +++ b/core/src/rope/mod.rs @@ -1,4 +1,5 @@ -use {super::fibbonacci::fibbonacci, std::rc::Rc}; +use super::fibbonacci::fibbonacci; +pub use std::rc::Rc; /// [Rope](https://en.wikipedia.org/wiki/Rope_(data_structure)) data structure /// implementation. diff --git a/core/src/rope/tests/command_list.rs b/core/src/rope/tests/command_list.rs new file mode 100644 index 0000000..981560b --- /dev/null +++ b/core/src/rope/tests/command_list.rs @@ -0,0 +1,69 @@ +use super::super::{Rc, Rope}; +use rand::{ + distributions::{Alphanumeric, DistString}, + rngs::SmallRng, + Rng, SeedableRng, +}; + +#[derive(Clone, Debug)] +pub enum Command { + InsertAtCharIndex { index: usize, text: String }, + DeleteAtCharIndex { index: usize, length: usize }, +} + +impl Command { + pub fn run(&self, rope: &Rc) -> Rc { + match self { + Command::InsertAtCharIndex { index, text } => rope.insert_at_char_index(*index, text), + Command::DeleteAtCharIndex { index, length } => { + rope.delete_at_char_index(*index, *length) + } + } + } + + pub fn run_on_string(&self, input: String) -> String { + match self { + Command::InsertAtCharIndex { index, text } => input + .chars() + .take(*index) + .chain(text.chars()) + .chain(input.chars().skip(*index)) + .collect(), + Command::DeleteAtCharIndex { index, length } => input + .chars() + .enumerate() + .filter(|(i, _)| *i < *index || *i >= index + length) + .map(|(_, c)| c) + .collect(), + } + } +} + +// TODO: Alphanumeric only samples ASCII +pub fn generate_random_edit_sequence_with_seed( + length: usize, + seed: u64, +) -> (String, Vec<(Command, String)>) { + let mut rng = SmallRng::seed_from_u64(seed); + let start_text_length = rng.gen_range(0..4000); + let start_text = Alphanumeric.sample_string(&mut rng, start_text_length); + let num_steps = rng.gen_range(0..1000); + let mut steps = Vec::with_capacity(num_steps); + let mut current_text = start_text.clone(); + for i in 0..num_steps { + let current_text_length = current_text.len(); + let command = if rng.gen_bool(0.7) || current_text_length > 0 { + let index = rng.gen_range(0..current_text_length); + let text_len = rng.gen_range(0..100); + let text = Alphanumeric.sample_string(&mut rng, text_len); + Command::InsertAtCharIndex { index, text } + } else { + let index = rng.gen_range(0..current_text_length-1); + let length = rng.gen_range(1..(current_text_length - index)); + Command::DeleteAtCharIndex { index, length } + }; + current_text = command.run_on_string(current_text); + steps.push((command.clone(), current_text.clone())); + } + (start_text, steps) +} diff --git a/core/src/rope/tests/mod.rs b/core/src/rope/tests/mod.rs index d7aaf5d..4c0a259 100644 --- a/core/src/rope/tests/mod.rs +++ b/core/src/rope/tests/mod.rs @@ -1,6 +1,8 @@ use super::*; use ntest::timeout; +mod command_list; + fn small_test_rope() -> Rc { Rope::join( Rope::join( @@ -469,3 +471,96 @@ fn char_iterator_reports_correct_point() { assert_eq!(found.column, expected_column); } } + +// Run multiple tests with different seeds so that `cargo test` will run them in +// parallel. +// TODO: This could use a macro to generate the list of tests. +fn random_insertions_and_deletions_with_seed(seed: u64) { + let (start_text, edits) = command_list::generate_random_edit_sequence_with_seed(1000, seed); + let mut target = Rope::new(start_text); + for (command, expected_text) in edits { + println!("{:?}", command); + target = command.run(&target); + assert_eq!(expected_text, target.iter_chars().collect::()); + } +} + +#[test] +fn random_insertions_and_deletions_1() { + random_insertions_and_deletions_with_seed(0x1234567890ab0001); +} + +#[test] +fn random_insertions_and_deletions_2() { + random_insertions_and_deletions_with_seed(0x1234567890ab0002); +} + +#[test] +fn random_insertions_and_deletions_3() { + random_insertions_and_deletions_with_seed(0x1234567890ab0003); +} + +#[test] +fn random_insertions_and_deletions_4() { + random_insertions_and_deletions_with_seed(0x1234567890ab0004); +} + +#[test] +fn random_insertions_and_deletions_5() { + random_insertions_and_deletions_with_seed(0x1234567890ab0005); +} + +#[test] +fn random_insertions_and_deletions_6() { + random_insertions_and_deletions_with_seed(0x1234567890ab0006); +} + +#[test] +fn random_insertions_and_deletions_7() { + random_insertions_and_deletions_with_seed(0x1234567890ab0007); +} + +#[test] +fn random_insertions_and_deletions_8() { + random_insertions_and_deletions_with_seed(0x1234567890ab0008); +} + +#[test] +fn random_insertions_and_deletions_9() { + random_insertions_and_deletions_with_seed(0x1234567890ab0009); +} + +#[test] +fn random_insertions_and_deletions_10() { + random_insertions_and_deletions_with_seed(0x1234567890ab0010); +} + +#[test] +fn random_insertions_and_deletions_11() { + random_insertions_and_deletions_with_seed(0x1234567890ab0011); +} + +#[test] +fn random_insertions_and_deletions_12() { + random_insertions_and_deletions_with_seed(0x1234567890ab0012); +} + +#[test] +fn random_insertions_and_deletions_13() { + random_insertions_and_deletions_with_seed(0x1234567890ab0013); +} + +#[test] +fn random_insertions_and_deletions_14() { + random_insertions_and_deletions_with_seed(0x1234567890ab0014); +} + +#[test] +fn random_insertions_and_deletions_15() { + random_insertions_and_deletions_with_seed(0x1234567890ab0015); +} + +#[test] +fn random_insertions_and_deletions_16() { + random_insertions_and_deletions_with_seed(0x1234567890ab0016); +}