From 02f4d8d39291c40679ace981837e508a3d0f8073 Mon Sep 17 00:00:00 2001 From: Shafkath Shuhan Date: Sun, 19 Feb 2023 10:42:32 -0500 Subject: [PATCH] write reload unit test --- Cargo.lock | 3 +- helix-core/src/history.rs | 23 +++++++-- helix-loader/src/lib.rs | 2 +- helix-view/Cargo.toml | 2 + helix-view/src/document.rs | 96 +++++++++++++++++++++++++++++++++++++- 5 files changed, 118 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 015f667853da8..3879c4d2d9db2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1124,7 +1124,6 @@ dependencies = [ "hashbrown 0.13.2", "helix-loader", "imara-diff", - "libc", "log", "once_cell", "quickcheck", @@ -1280,9 +1279,11 @@ dependencies = [ "libc", "log", "once_cell", + "quickcheck", "serde", "serde_json", "slotmap", + "tempfile", "tokio", "tokio-stream", "toml", diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index ac45ecf7adaa2..efa4dccaedf13 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -55,6 +55,7 @@ pub struct State { pub struct History { revisions: Vec, current: usize, + version: Option, } /// A single point in history. See [History] for more information. @@ -90,6 +91,7 @@ impl Default for History { timestamp: Instant::now(), }], current: 0, + version: None, } } } @@ -118,7 +120,7 @@ impl Revision { } // Temporarily 3 for review. -const HEADER_TAG: &str = "Helix Undofile 3\n"; +const HEADER_TAG: &str = "Helix Undofile 4\n"; fn get_hash(reader: &mut R) -> std::io::Result<[u8; 20]> { const BUF_SIZE: usize = 8192; @@ -149,12 +151,15 @@ impl History { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); + let version = self.version.map(|n| n.get()).unwrap_or(0); write_string(writer, HEADER_TAG)?; write_usize(writer, self.current)?; write_usize(writer, revision)?; write_u64(writer, mtime)?; + write_usize(writer, version)?; writer.write_all(&get_hash(&mut std::fs::File::open(path)?)?)?; + // Append new revisions to the end of the file. write_usize(writer, self.revisions.len())?; writer.seek(SeekFrom::End(0))?; for rev in &self.revisions[last_saved_revision..] { @@ -164,7 +169,7 @@ impl History { } pub fn deserialize(reader: &mut R, path: &Path) -> std::io::Result<(usize, Self)> { - let (current, last_saved_revision) = Self::read_header(reader, path)?; + let (current, last_saved_revision, version) = Self::read_header(reader, path)?; let timestamp = Instant::now(); let len = read_usize(reader)?; let mut revisions: Vec = Vec::with_capacity(len); @@ -179,7 +184,11 @@ impl History { revisions.push(res); } - let history = History { current, revisions }; + let history = History { + current, + revisions, + version, + }; Ok((last_saved_revision, history)) } @@ -200,7 +209,10 @@ impl History { Ok(()) } - pub fn read_header(reader: &mut R, path: &Path) -> std::io::Result<(usize, usize)> { + pub fn read_header( + reader: &mut R, + path: &Path, + ) -> std::io::Result<(usize, usize, Option)> { let header = read_string(reader)?; if HEADER_TAG != header { Err(std::io::Error::new( @@ -216,6 +228,7 @@ impl History { .duration_since(std::time::UNIX_EPOCH) .unwrap() .as_secs(); + let version = NonZeroUsize::new(read_usize(reader)?); let mut hash = [0u8; 20]; reader.read_exact(&mut hash)?; @@ -225,7 +238,7 @@ impl History { "outdated undo file", )); } - Ok((current, last_saved_revision)) + Ok((current, last_saved_revision, version)) } } } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index b61138c837a40..51d455891bc5b 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -70,7 +70,7 @@ pub fn local_config_dirs() -> Vec { } pub fn cache_dir() -> PathBuf { - let mut path = if cfg!(feature = "integration") { + let mut path = if cfg!(feature = "integration") || cfg!(test) { std::env::temp_dir() } else { // TODO: allow env var override diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 17e07e9a2dd6a..b22078eb51981 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -52,4 +52,6 @@ clipboard-win = { version = "4.5", features = ["std"] } libc = "0.2" [dev-dependencies] +quickcheck = { version = "1", default-features = false } +tempfile = "3" helix-tui = { path = "../helix-tui" } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 4ddbcf44eba72..77175820c5694 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -636,6 +636,7 @@ impl Document { .into_std() .await; tokio::task::spawn_blocking(move || -> anyhow::Result<()> { + // Truncate the file if it's not a valid undofile. if History::deserialize(&mut std::fs::File::open(&undofile_path)?, &path) .is_ok() { @@ -1386,7 +1387,8 @@ impl Display for FormatterError { #[cfg(test)] mod test { - use arc_swap::ArcSwap; + use arc_swap::{access::Map, ArcSwap}; + use quickcheck::Gen; use super::*; @@ -1556,6 +1558,98 @@ mod test { ); } + #[tokio::test(flavor = "multi_thread")] + async fn reload_history() { + let test_fn: fn(Vec) -> bool = |changes| -> bool { + let len = changes.len() / 3; + let mut original = Rope::new(); + let mut iter = changes.into_iter(); + + let changes_a: Vec<_> = iter + .by_ref() + .take(len) + .map(|c| { + let c = Rope::from(c); + let transaction = helix_core::diff::compare_ropes(&original, &c); + original = c; + transaction + }) + .collect(); + let mut original_concurrent = original.clone(); + + let changes_b: Vec<_> = iter + .by_ref() + .take(len) + .map(|c| { + let c = Rope::from(c); + let transaction = helix_core::diff::compare_ropes(&original, &c); + original = c; + transaction + }) + .collect(); + let changes_c: Vec<_> = iter + .take(len) + .map(|c| { + let c = Rope::from(c); + let transaction = helix_core::diff::compare_ropes(&original_concurrent, &c); + original_concurrent = c; + transaction + }) + .collect(); + + let file = tempfile::NamedTempFile::new().unwrap(); + let mut config = Config::default(); + config.persistent_undo = true; + + let view_id = ViewId::default(); + let config = Arc::new(ArcSwap::new(Arc::new(config))); + let mut doc_1 = Document::open(file.path(), None, None, config.clone()).unwrap(); + doc_1.ensure_view_init(view_id); + + // Make changes & save document A + for c in changes_a { + doc_1.apply(&c, view_id); + } + helix_lsp::block_on(doc_1.save::(None, true).unwrap()).unwrap(); + + let mut doc_2 = Document::open(file.path(), None, None, config.clone()).unwrap(); + let mut doc_3 = Document::open(file.path(), None, None, config.clone()).unwrap(); + doc_2.ensure_view_init(view_id); + doc_3.ensure_view_init(view_id); + + // Make changes in A and B at the same time. + for c in changes_b { + doc_1.apply(&c, view_id); + } + + for c in changes_c { + doc_2.apply(&c, view_id); + } + helix_lsp::block_on(doc_2.save::(None, true).unwrap()).unwrap(); + + doc_1.load_history().unwrap(); + doc_3.load_history().unwrap(); + + assert_eq!(doc_2.history.get_mut(), doc_3.history.get_mut()); + + helix_lsp::block_on(doc_1.save::(None, true).unwrap()).unwrap(); + doc_2.load_history().unwrap(); + doc_3.load_history().unwrap(); + doc_1.history.get_mut() == doc_2.history.get_mut() + && doc_1.history.get_mut() == doc_3.history.get_mut() + }; + let handles: Vec<_> = (0..100) + .map(|_| { + tokio::task::spawn_blocking(move || { + quickcheck::QuickCheck::new() + .max_tests(1) + .quickcheck(test_fn); + }) + }) + .collect(); + futures_util::future::try_join_all(handles).await.unwrap(); + } + macro_rules! decode { ($name:ident, $label:expr, $label_override:expr) => { #[test]