diff --git a/Cargo.lock b/Cargo.lock index aace3b782bcc2cbf04a102dbb1fa64155df5abd0..6e4f810b452392db187c7f9bebeabdaea09e93ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6498,6 +6498,7 @@ dependencies = [ "async-trait", "cocoa 0.26.0", "collections", + "fs", "fsevent", "futures 0.3.31", "git", diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml index 52063eeddcc3aa74adae33f3a78c74ecb6b6f04c..c38a4584109edf422d754115399566f987cb8ea6 100644 --- a/crates/fs/Cargo.toml +++ b/crates/fs/Cargo.toml @@ -10,6 +10,12 @@ workspace = true [lib] path = "src/fs.rs" +test = false + +[[test]] +name = "integration" +required-features = ["test-support"] +path = "tests/integration/main.rs" [dependencies] anyhow.workspace = true @@ -50,6 +56,7 @@ windows.workspace = true ashpd.workspace = true [dev-dependencies] +fs = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } git = { workspace = true, features = ["test-support"] } diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 1d20bd520b73b6e9154393b00435b5e66c438aff..eda6fcec200cef450ed4c4243e2b9ad3656f0f0d 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -738,65 +738,3 @@ impl GitRepository for FakeGitRepository { }) } } - -#[cfg(test)] -mod tests { - use crate::{FakeFs, Fs}; - use gpui::BackgroundExecutor; - use serde_json::json; - use std::path::Path; - use util::path; - - #[gpui::test] - async fn test_checkpoints(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/"), - json!({ - "bar": { - "baz": "qux" - }, - "foo": { - ".git": {}, - "a": "lorem", - "b": "ipsum", - }, - }), - ) - .await; - fs.with_git_state(Path::new("/foo/.git"), true, |_git| {}) - .unwrap(); - let repository = fs - .open_repo(Path::new("/foo/.git"), Some("git".as_ref())) - .unwrap(); - - let checkpoint_1 = repository.checkpoint().await.unwrap(); - fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap(); - fs.write(Path::new("/foo/c"), b"dolor").await.unwrap(); - let checkpoint_2 = repository.checkpoint().await.unwrap(); - let checkpoint_3 = repository.checkpoint().await.unwrap(); - - assert!( - repository - .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone()) - .await - .unwrap() - ); - assert!( - !repository - .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone()) - .await - .unwrap() - ); - - repository.restore_checkpoint(checkpoint_1).await.unwrap(); - assert_eq!( - fs.files_with_contents(Path::new("")), - [ - (Path::new(path!("/bar/baz")).into(), b"qux".into()), - (Path::new(path!("/foo/a")).into(), b"lorem".into()), - (Path::new(path!("/foo/b")).into(), b"ipsum".into()) - ] - ); - } -} diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index 27fc3adf31ca2f680ad3256496969fa25dabca49..ea09aa27abd1a8d0d1d9a6f587fe24d90e5f0d1f 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -46,21 +46,21 @@ use std::{ use tempfile::TempDir; use text::LineEnding; -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] mod fake_git_repo; -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] use collections::{BTreeMap, btree_map}; -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] use fake_git_repo::FakeGitRepositoryState; -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] use git::{ repository::{RepoPath, repo_path}, status::{FileStatus, StatusCode, TrackedStatus, UnmergedStatus}, }; -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] use smol::io::AsyncReadExt; -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] use std::ffi::OsStr; pub trait Watcher: Send + Sync { @@ -152,7 +152,7 @@ pub trait Fs: Send + Sync { async fn is_case_sensitive(&self) -> Result; fn subscribe_to_jobs(&self) -> JobEventReceiver; - #[cfg(any(test, feature = "test-support"))] + #[cfg(feature = "test-support")] fn as_fake(&self) -> Arc { panic!("called as_fake on a real fs"); } @@ -1223,7 +1223,7 @@ impl Watcher for RealWatcher { } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] pub struct FakeFs { this: std::sync::Weak, // Use an unfair lock to ensure tests are deterministic. @@ -1231,7 +1231,7 @@ pub struct FakeFs { executor: gpui::BackgroundExecutor, } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] struct FakeFsState { root: FakeFsEntry, next_inode: u64, @@ -1247,7 +1247,7 @@ struct FakeFsState { job_event_subscribers: Arc>>, } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] #[derive(Clone, Debug)] enum FakeFsEntry { File { @@ -1270,7 +1270,7 @@ enum FakeFsEntry { }, } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] impl PartialEq for FakeFsEntry { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -1331,7 +1331,7 @@ impl PartialEq for FakeFsEntry { } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] impl FakeFsState { fn get_and_increment_mtime(&mut self) -> MTime { let mtime = self.next_mtime; @@ -1495,11 +1495,11 @@ impl FakeFsState { } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] pub static FS_DOT_GIT: std::sync::LazyLock<&'static OsStr> = std::sync::LazyLock::new(|| OsStr::new(".git")); -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] impl FakeFs { /// We need to use something large enough for Windows and Unix to consider this a new file. /// https://doc.rust-lang.org/nightly/std/time/struct.SystemTime.html#platform-specific-behavior @@ -1617,50 +1617,58 @@ impl FakeFs { new_content: Vec, recreate_inode: bool, ) -> Result<()> { - let mut state = self.state.lock(); - let path_buf = path.as_ref().to_path_buf(); - *state.path_write_counts.entry(path_buf).or_insert(0) += 1; - let new_inode = state.get_and_increment_inode(); - let new_mtime = state.get_and_increment_mtime(); - let new_len = new_content.len() as u64; - let mut kind = None; - state.write_path(path.as_ref(), |entry| { - match entry { - btree_map::Entry::Vacant(e) => { - kind = Some(PathEventKind::Created); - e.insert(FakeFsEntry::File { - inode: new_inode, - mtime: new_mtime, - len: new_len, - content: new_content, - git_dir_path: None, - }); - } - btree_map::Entry::Occupied(mut e) => { - kind = Some(PathEventKind::Changed); - if let FakeFsEntry::File { - inode, - mtime, - len, - content, - .. - } = e.get_mut() - { - *mtime = new_mtime; - *content = new_content; - *len = new_len; - if recreate_inode { - *inode = new_inode; + fn inner( + this: &FakeFs, + path: &Path, + new_content: Vec, + recreate_inode: bool, + ) -> Result<()> { + let mut state = this.state.lock(); + let path_buf = path.to_path_buf(); + *state.path_write_counts.entry(path_buf).or_insert(0) += 1; + let new_inode = state.get_and_increment_inode(); + let new_mtime = state.get_and_increment_mtime(); + let new_len = new_content.len() as u64; + let mut kind = None; + state.write_path(path, |entry| { + match entry { + btree_map::Entry::Vacant(e) => { + kind = Some(PathEventKind::Created); + e.insert(FakeFsEntry::File { + inode: new_inode, + mtime: new_mtime, + len: new_len, + content: new_content, + git_dir_path: None, + }); + } + btree_map::Entry::Occupied(mut e) => { + kind = Some(PathEventKind::Changed); + if let FakeFsEntry::File { + inode, + mtime, + len, + content, + .. + } = e.get_mut() + { + *mtime = new_mtime; + *content = new_content; + *len = new_len; + if recreate_inode { + *inode = new_inode; + } + } else { + anyhow::bail!("not a file") } - } else { - anyhow::bail!("not a file") } } - } + Ok(()) + })?; + state.emit_event([(path, kind)]); Ok(()) - })?; - state.emit_event([(path.as_ref(), kind)]); - Ok(()) + } + inner(self, path.as_ref(), new_content, recreate_inode) } pub fn read_file_sync(&self, path: impl AsRef) -> Result> { @@ -1725,30 +1733,35 @@ impl FakeFs { use futures::FutureExt as _; use serde_json::Value::*; - async move { - let path = path.as_ref(); - - match tree { - Object(map) => { - self.create_dir(path).await.unwrap(); - for (name, contents) in map { - let mut path = PathBuf::from(path); - path.push(name); - self.insert_tree(&path, contents).await; + fn inner<'a>( + this: &'a FakeFs, + path: Arc, + tree: serde_json::Value, + ) -> futures::future::BoxFuture<'a, ()> { + async move { + match tree { + Object(map) => { + this.create_dir(&path).await.unwrap(); + for (name, contents) in map { + let mut path = PathBuf::from(path.as_ref()); + path.push(name); + this.insert_tree(&path, contents).await; + } + } + Null => { + this.create_dir(&path).await.unwrap(); + } + String(contents) => { + this.insert_file(&path, contents.into_bytes()).await; + } + _ => { + panic!("JSON object must contain only objects, strings, or null"); } - } - Null => { - self.create_dir(path).await.unwrap(); - } - String(contents) => { - self.insert_file(&path, contents.into_bytes()).await; - } - _ => { - panic!("JSON object must contain only objects, strings, or null"); } } + .boxed() } - .boxed() + inner(self, Arc::from(path.as_ref()), tree) } pub fn insert_tree_from_real_fs<'a>( @@ -2205,7 +2218,7 @@ impl FakeFs { } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] impl FakeFsEntry { fn is_file(&self) -> bool { matches!(self, Self::File { .. }) @@ -2232,7 +2245,7 @@ impl FakeFsEntry { } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] struct FakeWatcher { tx: smol::channel::Sender>, original_path: PathBuf, @@ -2240,7 +2253,7 @@ struct FakeWatcher { prefixes: Mutex>, } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] impl Watcher for FakeWatcher { fn add(&self, path: &Path) -> Result<()> { if path.starts_with(&self.original_path) { @@ -2260,13 +2273,13 @@ impl Watcher for FakeWatcher { } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] #[derive(Debug)] struct FakeHandle { inode: u64, } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] impl FileHandle for FakeHandle { fn current_path(&self, fs: &Arc) -> Result { let fs = fs.as_fake(); @@ -2282,7 +2295,7 @@ impl FileHandle for FakeHandle { } } -#[cfg(any(test, feature = "test-support"))] +#[cfg(feature = "test-support")] #[async_trait::async_trait] impl Fs for FakeFs { async fn create_dir(&self, path: &Path) -> Result<()> { @@ -2810,7 +2823,7 @@ impl Fs for FakeFs { receiver } - #[cfg(any(test, feature = "test-support"))] + #[cfg(feature = "test-support")] fn as_fake(&self) -> Arc { self.this.upgrade().unwrap() } @@ -2977,590 +2990,3 @@ fn atomic_replace>( ) } } - -#[cfg(test)] -mod tests { - use super::*; - use gpui::BackgroundExecutor; - use serde_json::json; - use util::path; - - #[gpui::test] - async fn test_fake_fs(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "a": "A", - "b": "B" - }, - "dir2": { - "c": "C", - "dir3": { - "d": "D" - } - } - }), - ) - .await; - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/root/dir1/a")), - PathBuf::from(path!("/root/dir1/b")), - PathBuf::from(path!("/root/dir2/c")), - PathBuf::from(path!("/root/dir2/dir3/d")), - ] - ); - - fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into()) - .await - .unwrap(); - - assert_eq!( - fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref()) - .await - .unwrap(), - PathBuf::from(path!("/root/dir2/dir3")), - ); - assert_eq!( - fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref()) - .await - .unwrap(), - PathBuf::from(path!("/root/dir2/dir3/d")), - ); - assert_eq!( - fs.load(path!("/root/dir2/link-to-dir3/d").as_ref()) - .await - .unwrap(), - "D", - ); - } - - #[gpui::test] - async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor.clone()); - fs.insert_tree( - path!("/outer"), - json!({ - "a": "A", - "b": "B", - "inner": {} - }), - ) - .await; - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/a")), - PathBuf::from(path!("/outer/b")), - ] - ); - - let source = Path::new(path!("/outer/a")); - let target = Path::new(path!("/outer/a copy")); - copy_recursive(fs.as_ref(), source, target, Default::default()) - .await - .unwrap(); - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/a")), - PathBuf::from(path!("/outer/a copy")), - PathBuf::from(path!("/outer/b")), - ] - ); - - let source = Path::new(path!("/outer/a")); - let target = Path::new(path!("/outer/inner/a copy")); - copy_recursive(fs.as_ref(), source, target, Default::default()) - .await - .unwrap(); - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/a")), - PathBuf::from(path!("/outer/a copy")), - PathBuf::from(path!("/outer/b")), - PathBuf::from(path!("/outer/inner/a copy")), - ] - ); - } - - #[gpui::test] - async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor.clone()); - fs.insert_tree( - path!("/outer"), - json!({ - "a": "A", - "empty": {}, - "non-empty": { - "b": "B", - } - }), - ) - .await; - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/a")), - PathBuf::from(path!("/outer/non-empty/b")), - ] - ); - assert_eq!( - fs.directories(false), - vec![ - PathBuf::from(path!("/")), - PathBuf::from(path!("/outer")), - PathBuf::from(path!("/outer/empty")), - PathBuf::from(path!("/outer/non-empty")), - ] - ); - - let source = Path::new(path!("/outer/empty")); - let target = Path::new(path!("/outer/empty copy")); - copy_recursive(fs.as_ref(), source, target, Default::default()) - .await - .unwrap(); - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/a")), - PathBuf::from(path!("/outer/non-empty/b")), - ] - ); - assert_eq!( - fs.directories(false), - vec![ - PathBuf::from(path!("/")), - PathBuf::from(path!("/outer")), - PathBuf::from(path!("/outer/empty")), - PathBuf::from(path!("/outer/empty copy")), - PathBuf::from(path!("/outer/non-empty")), - ] - ); - - let source = Path::new(path!("/outer/non-empty")); - let target = Path::new(path!("/outer/non-empty copy")); - copy_recursive(fs.as_ref(), source, target, Default::default()) - .await - .unwrap(); - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/a")), - PathBuf::from(path!("/outer/non-empty/b")), - PathBuf::from(path!("/outer/non-empty copy/b")), - ] - ); - assert_eq!( - fs.directories(false), - vec![ - PathBuf::from(path!("/")), - PathBuf::from(path!("/outer")), - PathBuf::from(path!("/outer/empty")), - PathBuf::from(path!("/outer/empty copy")), - PathBuf::from(path!("/outer/non-empty")), - PathBuf::from(path!("/outer/non-empty copy")), - ] - ); - } - - #[gpui::test] - async fn test_copy_recursive(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor.clone()); - fs.insert_tree( - path!("/outer"), - json!({ - "inner1": { - "a": "A", - "b": "B", - "inner3": { - "d": "D", - }, - "inner4": {} - }, - "inner2": { - "c": "C", - } - }), - ) - .await; - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/b")), - PathBuf::from(path!("/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/inner3/d")), - ] - ); - assert_eq!( - fs.directories(false), - vec![ - PathBuf::from(path!("/")), - PathBuf::from(path!("/outer")), - PathBuf::from(path!("/outer/inner1")), - PathBuf::from(path!("/outer/inner2")), - PathBuf::from(path!("/outer/inner1/inner3")), - PathBuf::from(path!("/outer/inner1/inner4")), - ] - ); - - let source = Path::new(path!("/outer")); - let target = Path::new(path!("/outer/inner1/outer")); - copy_recursive(fs.as_ref(), source, target, Default::default()) - .await - .unwrap(); - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/b")), - PathBuf::from(path!("/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/inner3/d")), - PathBuf::from(path!("/outer/inner1/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/outer/inner1/b")), - PathBuf::from(path!("/outer/inner1/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")), - ] - ); - assert_eq!( - fs.directories(false), - vec![ - PathBuf::from(path!("/")), - PathBuf::from(path!("/outer")), - PathBuf::from(path!("/outer/inner1")), - PathBuf::from(path!("/outer/inner2")), - PathBuf::from(path!("/outer/inner1/inner3")), - PathBuf::from(path!("/outer/inner1/inner4")), - PathBuf::from(path!("/outer/inner1/outer")), - PathBuf::from(path!("/outer/inner1/outer/inner1")), - PathBuf::from(path!("/outer/inner1/outer/inner2")), - PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")), - PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")), - ] - ); - } - - #[gpui::test] - async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor.clone()); - fs.insert_tree( - path!("/outer"), - json!({ - "inner1": { - "a": "A", - "b": "B", - "outer": { - "inner1": { - "a": "B" - } - } - }, - "inner2": { - "c": "C", - } - }), - ) - .await; - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/b")), - PathBuf::from(path!("/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/outer/inner1/a")), - ] - ); - assert_eq!( - fs.load(path!("/outer/inner1/outer/inner1/a").as_ref()) - .await - .unwrap(), - "B", - ); - - let source = Path::new(path!("/outer")); - let target = Path::new(path!("/outer/inner1/outer")); - copy_recursive( - fs.as_ref(), - source, - target, - CopyOptions { - overwrite: true, - ..Default::default() - }, - ) - .await - .unwrap(); - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/b")), - PathBuf::from(path!("/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/outer/inner1/b")), - PathBuf::from(path!("/outer/inner1/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")), - ] - ); - assert_eq!( - fs.load(path!("/outer/inner1/outer/inner1/a").as_ref()) - .await - .unwrap(), - "A" - ); - } - - #[gpui::test] - async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor.clone()); - fs.insert_tree( - path!("/outer"), - json!({ - "inner1": { - "a": "A", - "b": "B", - "outer": { - "inner1": { - "a": "B" - } - } - }, - "inner2": { - "c": "C", - } - }), - ) - .await; - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/b")), - PathBuf::from(path!("/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/outer/inner1/a")), - ] - ); - assert_eq!( - fs.load(path!("/outer/inner1/outer/inner1/a").as_ref()) - .await - .unwrap(), - "B", - ); - - let source = Path::new(path!("/outer")); - let target = Path::new(path!("/outer/inner1/outer")); - copy_recursive( - fs.as_ref(), - source, - target, - CopyOptions { - ignore_if_exists: true, - ..Default::default() - }, - ) - .await - .unwrap(); - - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/b")), - PathBuf::from(path!("/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/outer/inner1/a")), - PathBuf::from(path!("/outer/inner1/outer/inner1/b")), - PathBuf::from(path!("/outer/inner1/outer/inner2/c")), - PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")), - ] - ); - assert_eq!( - fs.load(path!("/outer/inner1/outer/inner1/a").as_ref()) - .await - .unwrap(), - "B" - ); - } - - #[gpui::test] - async fn test_realfs_atomic_write(executor: BackgroundExecutor) { - // With the file handle still open, the file should be replaced - // https://github.com/zed-industries/zed/issues/30054 - let fs = RealFs { - bundled_git_binary_path: None, - executor, - next_job_id: Arc::new(AtomicUsize::new(0)), - job_event_subscribers: Arc::new(Mutex::new(Vec::new())), - }; - let temp_dir = TempDir::new().unwrap(); - let file_to_be_replaced = temp_dir.path().join("file.txt"); - let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap(); - file.write_all(b"Hello").unwrap(); - // drop(file); // We still hold the file handle here - let content = std::fs::read_to_string(&file_to_be_replaced).unwrap(); - assert_eq!(content, "Hello"); - smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap(); - let content = std::fs::read_to_string(&file_to_be_replaced).unwrap(); - assert_eq!(content, "World"); - } - - #[gpui::test] - async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) { - let fs = RealFs { - bundled_git_binary_path: None, - executor, - next_job_id: Arc::new(AtomicUsize::new(0)), - job_event_subscribers: Arc::new(Mutex::new(Vec::new())), - }; - let temp_dir = TempDir::new().unwrap(); - let file_to_be_replaced = temp_dir.path().join("file.txt"); - smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap(); - let content = std::fs::read_to_string(&file_to_be_replaced).unwrap(); - assert_eq!(content, "Hello"); - } - - #[gpui::test] - #[cfg(target_os = "windows")] - async fn test_realfs_canonicalize(executor: BackgroundExecutor) { - use util::paths::SanitizedPath; - - let fs = RealFs { - bundled_git_binary_path: None, - executor, - next_job_id: Arc::new(AtomicUsize::new(0)), - job_event_subscribers: Arc::new(Mutex::new(Vec::new())), - }; - let temp_dir = TempDir::new().unwrap(); - let file = temp_dir.path().join("test (1).txt"); - let file = SanitizedPath::new(&file); - std::fs::write(&file, "test").unwrap(); - - let canonicalized = fs.canonicalize(file.as_path()).await; - assert!(canonicalized.is_ok()); - } - - #[gpui::test] - async fn test_rename(executor: BackgroundExecutor) { - let fs = FakeFs::new(executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "src": { - "file_a.txt": "content a", - "file_b.txt": "content b" - } - }), - ) - .await; - - fs.rename( - Path::new(path!("/root/src/file_a.txt")), - Path::new(path!("/root/src/new/renamed_a.txt")), - RenameOptions { - create_parents: true, - ..Default::default() - }, - ) - .await - .unwrap(); - - // Assert that the `file_a.txt` file was being renamed and moved to a - // different directory that did not exist before. - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/root/src/file_b.txt")), - PathBuf::from(path!("/root/src/new/renamed_a.txt")), - ] - ); - - let result = fs - .rename( - Path::new(path!("/root/src/file_b.txt")), - Path::new(path!("/root/src/old/renamed_b.txt")), - RenameOptions { - create_parents: false, - ..Default::default() - }, - ) - .await; - - // Assert that the `file_b.txt` file was not renamed nor moved, as - // `create_parents` was set to `false`. - // different directory that did not exist before. - assert!(result.is_err()); - assert_eq!( - fs.files(), - vec![ - PathBuf::from(path!("/root/src/file_b.txt")), - PathBuf::from(path!("/root/src/new/renamed_a.txt")), - ] - ); - } - - #[gpui::test] - #[cfg(unix)] - async fn test_realfs_broken_symlink_metadata(executor: BackgroundExecutor) { - let tempdir = TempDir::new().unwrap(); - let path = tempdir.path(); - let fs = RealFs { - bundled_git_binary_path: None, - executor, - next_job_id: Arc::new(AtomicUsize::new(0)), - job_event_subscribers: Arc::new(Mutex::new(Vec::new())), - }; - let symlink_path = path.join("symlink"); - smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("file_a.txt"))).unwrap(); - let metadata = fs - .metadata(&symlink_path) - .await - .expect("metadata call succeeds") - .expect("metadata returned"); - assert!(metadata.is_symlink); - assert!(!metadata.is_dir); - assert!(!metadata.is_fifo); - assert!(!metadata.is_executable); - // don't care about len or mtime on symlinks? - } - - #[gpui::test] - #[cfg(unix)] - async fn test_realfs_symlink_loop_metadata(executor: BackgroundExecutor) { - let tempdir = TempDir::new().unwrap(); - let path = tempdir.path(); - let fs = RealFs { - bundled_git_binary_path: None, - executor, - next_job_id: Arc::new(AtomicUsize::new(0)), - job_event_subscribers: Arc::new(Mutex::new(Vec::new())), - }; - let symlink_path = path.join("symlink"); - smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("symlink"))).unwrap(); - let metadata = fs - .metadata(&symlink_path) - .await - .expect("metadata call succeeds") - .expect("metadata returned"); - assert!(metadata.is_symlink); - assert!(!metadata.is_dir); - assert!(!metadata.is_fifo); - assert!(!metadata.is_executable); - // don't care about len or mtime on symlinks? - } -} diff --git a/crates/fs/tests/integration/fake_git_repo.rs b/crates/fs/tests/integration/fake_git_repo.rs new file mode 100644 index 0000000000000000000000000000000000000000..36dfcaf168b4f0190c5c49bf4798fac7bc9bd37b --- /dev/null +++ b/crates/fs/tests/integration/fake_git_repo.rs @@ -0,0 +1,58 @@ +use fs::{FakeFs, Fs}; +use gpui::BackgroundExecutor; +use serde_json::json; +use std::path::Path; +use util::path; + +#[gpui::test] +async fn test_checkpoints(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor); + fs.insert_tree( + path!("/"), + json!({ + "bar": { + "baz": "qux" + }, + "foo": { + ".git": {}, + "a": "lorem", + "b": "ipsum", + }, + }), + ) + .await; + fs.with_git_state(Path::new("/foo/.git"), true, |_git| {}) + .unwrap(); + let repository = fs + .open_repo(Path::new("/foo/.git"), Some("git".as_ref())) + .unwrap(); + + let checkpoint_1 = repository.checkpoint().await.unwrap(); + fs.write(Path::new("/foo/b"), b"IPSUM").await.unwrap(); + fs.write(Path::new("/foo/c"), b"dolor").await.unwrap(); + let checkpoint_2 = repository.checkpoint().await.unwrap(); + let checkpoint_3 = repository.checkpoint().await.unwrap(); + + assert!( + repository + .compare_checkpoints(checkpoint_2.clone(), checkpoint_3.clone()) + .await + .unwrap() + ); + assert!( + !repository + .compare_checkpoints(checkpoint_1.clone(), checkpoint_2.clone()) + .await + .unwrap() + ); + + repository.restore_checkpoint(checkpoint_1).await.unwrap(); + assert_eq!( + fs.files_with_contents(Path::new("")), + [ + (Path::new(path!("/bar/baz")).into(), b"qux".into()), + (Path::new(path!("/foo/a")).into(), b"lorem".into()), + (Path::new(path!("/foo/b")).into(), b"ipsum".into()) + ] + ); +} diff --git a/crates/fs/tests/integration/fs.rs b/crates/fs/tests/integration/fs.rs new file mode 100644 index 0000000000000000000000000000000000000000..dd5e694e23c99716a81b27afd487e3a6ea648209 --- /dev/null +++ b/crates/fs/tests/integration/fs.rs @@ -0,0 +1,564 @@ +use std::{ + io::Write, + path::{Path, PathBuf}, +}; + +use fs::*; +use gpui::BackgroundExecutor; +use serde_json::json; +use tempfile::TempDir; +use util::path; + +#[gpui::test] +async fn test_fake_fs(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/root"), + json!({ + "dir1": { + "a": "A", + "b": "B" + }, + "dir2": { + "c": "C", + "dir3": { + "d": "D" + } + } + }), + ) + .await; + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/root/dir1/a")), + PathBuf::from(path!("/root/dir1/b")), + PathBuf::from(path!("/root/dir2/c")), + PathBuf::from(path!("/root/dir2/dir3/d")), + ] + ); + + fs.create_symlink(path!("/root/dir2/link-to-dir3").as_ref(), "./dir3".into()) + .await + .unwrap(); + + assert_eq!( + fs.canonicalize(path!("/root/dir2/link-to-dir3").as_ref()) + .await + .unwrap(), + PathBuf::from(path!("/root/dir2/dir3")), + ); + assert_eq!( + fs.canonicalize(path!("/root/dir2/link-to-dir3/d").as_ref()) + .await + .unwrap(), + PathBuf::from(path!("/root/dir2/dir3/d")), + ); + assert_eq!( + fs.load(path!("/root/dir2/link-to-dir3/d").as_ref()) + .await + .unwrap(), + "D", + ); +} + +#[gpui::test] +async fn test_copy_recursive_with_single_file(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/outer"), + json!({ + "a": "A", + "b": "B", + "inner": {} + }), + ) + .await; + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/a")), + PathBuf::from(path!("/outer/b")), + ] + ); + + let source = Path::new(path!("/outer/a")); + let target = Path::new(path!("/outer/a copy")); + copy_recursive(fs.as_ref(), source, target, Default::default()) + .await + .unwrap(); + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/a")), + PathBuf::from(path!("/outer/a copy")), + PathBuf::from(path!("/outer/b")), + ] + ); + + let source = Path::new(path!("/outer/a")); + let target = Path::new(path!("/outer/inner/a copy")); + copy_recursive(fs.as_ref(), source, target, Default::default()) + .await + .unwrap(); + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/a")), + PathBuf::from(path!("/outer/a copy")), + PathBuf::from(path!("/outer/b")), + PathBuf::from(path!("/outer/inner/a copy")), + ] + ); +} + +#[gpui::test] +async fn test_copy_recursive_with_single_dir(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/outer"), + json!({ + "a": "A", + "empty": {}, + "non-empty": { + "b": "B", + } + }), + ) + .await; + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/a")), + PathBuf::from(path!("/outer/non-empty/b")), + ] + ); + assert_eq!( + fs.directories(false), + vec![ + PathBuf::from(path!("/")), + PathBuf::from(path!("/outer")), + PathBuf::from(path!("/outer/empty")), + PathBuf::from(path!("/outer/non-empty")), + ] + ); + + let source = Path::new(path!("/outer/empty")); + let target = Path::new(path!("/outer/empty copy")); + copy_recursive(fs.as_ref(), source, target, Default::default()) + .await + .unwrap(); + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/a")), + PathBuf::from(path!("/outer/non-empty/b")), + ] + ); + assert_eq!( + fs.directories(false), + vec![ + PathBuf::from(path!("/")), + PathBuf::from(path!("/outer")), + PathBuf::from(path!("/outer/empty")), + PathBuf::from(path!("/outer/empty copy")), + PathBuf::from(path!("/outer/non-empty")), + ] + ); + + let source = Path::new(path!("/outer/non-empty")); + let target = Path::new(path!("/outer/non-empty copy")); + copy_recursive(fs.as_ref(), source, target, Default::default()) + .await + .unwrap(); + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/a")), + PathBuf::from(path!("/outer/non-empty/b")), + PathBuf::from(path!("/outer/non-empty copy/b")), + ] + ); + assert_eq!( + fs.directories(false), + vec![ + PathBuf::from(path!("/")), + PathBuf::from(path!("/outer")), + PathBuf::from(path!("/outer/empty")), + PathBuf::from(path!("/outer/empty copy")), + PathBuf::from(path!("/outer/non-empty")), + PathBuf::from(path!("/outer/non-empty copy")), + ] + ); +} + +#[gpui::test] +async fn test_copy_recursive(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/outer"), + json!({ + "inner1": { + "a": "A", + "b": "B", + "inner3": { + "d": "D", + }, + "inner4": {} + }, + "inner2": { + "c": "C", + } + }), + ) + .await; + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/b")), + PathBuf::from(path!("/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/inner3/d")), + ] + ); + assert_eq!( + fs.directories(false), + vec![ + PathBuf::from(path!("/")), + PathBuf::from(path!("/outer")), + PathBuf::from(path!("/outer/inner1")), + PathBuf::from(path!("/outer/inner2")), + PathBuf::from(path!("/outer/inner1/inner3")), + PathBuf::from(path!("/outer/inner1/inner4")), + ] + ); + + let source = Path::new(path!("/outer")); + let target = Path::new(path!("/outer/inner1/outer")); + copy_recursive(fs.as_ref(), source, target, Default::default()) + .await + .unwrap(); + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/b")), + PathBuf::from(path!("/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/inner3/d")), + PathBuf::from(path!("/outer/inner1/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/outer/inner1/b")), + PathBuf::from(path!("/outer/inner1/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/outer/inner1/inner3/d")), + ] + ); + assert_eq!( + fs.directories(false), + vec![ + PathBuf::from(path!("/")), + PathBuf::from(path!("/outer")), + PathBuf::from(path!("/outer/inner1")), + PathBuf::from(path!("/outer/inner2")), + PathBuf::from(path!("/outer/inner1/inner3")), + PathBuf::from(path!("/outer/inner1/inner4")), + PathBuf::from(path!("/outer/inner1/outer")), + PathBuf::from(path!("/outer/inner1/outer/inner1")), + PathBuf::from(path!("/outer/inner1/outer/inner2")), + PathBuf::from(path!("/outer/inner1/outer/inner1/inner3")), + PathBuf::from(path!("/outer/inner1/outer/inner1/inner4")), + ] + ); +} + +#[gpui::test] +async fn test_copy_recursive_with_overwriting(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/outer"), + json!({ + "inner1": { + "a": "A", + "b": "B", + "outer": { + "inner1": { + "a": "B" + } + } + }, + "inner2": { + "c": "C", + } + }), + ) + .await; + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/b")), + PathBuf::from(path!("/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/outer/inner1/a")), + ] + ); + assert_eq!( + fs.load(path!("/outer/inner1/outer/inner1/a").as_ref()) + .await + .unwrap(), + "B", + ); + + let source = Path::new(path!("/outer")); + let target = Path::new(path!("/outer/inner1/outer")); + copy_recursive( + fs.as_ref(), + source, + target, + CopyOptions { + overwrite: true, + ..Default::default() + }, + ) + .await + .unwrap(); + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/b")), + PathBuf::from(path!("/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/outer/inner1/b")), + PathBuf::from(path!("/outer/inner1/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")), + ] + ); + assert_eq!( + fs.load(path!("/outer/inner1/outer/inner1/a").as_ref()) + .await + .unwrap(), + "A" + ); +} + +#[gpui::test] +async fn test_copy_recursive_with_ignoring(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/outer"), + json!({ + "inner1": { + "a": "A", + "b": "B", + "outer": { + "inner1": { + "a": "B" + } + } + }, + "inner2": { + "c": "C", + } + }), + ) + .await; + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/b")), + PathBuf::from(path!("/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/outer/inner1/a")), + ] + ); + assert_eq!( + fs.load(path!("/outer/inner1/outer/inner1/a").as_ref()) + .await + .unwrap(), + "B", + ); + + let source = Path::new(path!("/outer")); + let target = Path::new(path!("/outer/inner1/outer")); + copy_recursive( + fs.as_ref(), + source, + target, + CopyOptions { + ignore_if_exists: true, + ..Default::default() + }, + ) + .await + .unwrap(); + + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/b")), + PathBuf::from(path!("/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/outer/inner1/a")), + PathBuf::from(path!("/outer/inner1/outer/inner1/b")), + PathBuf::from(path!("/outer/inner1/outer/inner2/c")), + PathBuf::from(path!("/outer/inner1/outer/inner1/outer/inner1/a")), + ] + ); + assert_eq!( + fs.load(path!("/outer/inner1/outer/inner1/a").as_ref()) + .await + .unwrap(), + "B" + ); +} + +#[gpui::test] +async fn test_realfs_atomic_write(executor: BackgroundExecutor) { + // With the file handle still open, the file should be replaced + // https://github.com/zed-industries/zed/issues/30054 + let fs = RealFs::new(None, executor); + let temp_dir = TempDir::new().unwrap(); + let file_to_be_replaced = temp_dir.path().join("file.txt"); + let mut file = std::fs::File::create_new(&file_to_be_replaced).unwrap(); + file.write_all(b"Hello").unwrap(); + // drop(file); // We still hold the file handle here + let content = std::fs::read_to_string(&file_to_be_replaced).unwrap(); + assert_eq!(content, "Hello"); + smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "World".into())).unwrap(); + let content = std::fs::read_to_string(&file_to_be_replaced).unwrap(); + assert_eq!(content, "World"); +} + +#[gpui::test] +async fn test_realfs_atomic_write_non_existing_file(executor: BackgroundExecutor) { + let fs = RealFs::new(None, executor); + let temp_dir = TempDir::new().unwrap(); + let file_to_be_replaced = temp_dir.path().join("file.txt"); + smol::block_on(fs.atomic_write(file_to_be_replaced.clone(), "Hello".into())).unwrap(); + let content = std::fs::read_to_string(&file_to_be_replaced).unwrap(); + assert_eq!(content, "Hello"); +} + +#[gpui::test] +#[cfg(target_os = "windows")] +async fn test_realfs_canonicalize(executor: BackgroundExecutor) { + use util::paths::SanitizedPath; + + let fs = RealFs::new(None, executor); + let temp_dir = TempDir::new().unwrap(); + let file = temp_dir.path().join("test (1).txt"); + let file = SanitizedPath::new(&file); + std::fs::write(&file, "test").unwrap(); + + let canonicalized = fs.canonicalize(file.as_path()).await; + assert!(canonicalized.is_ok()); +} + +#[gpui::test] +async fn test_rename(executor: BackgroundExecutor) { + let fs = FakeFs::new(executor.clone()); + fs.insert_tree( + path!("/root"), + json!({ + "src": { + "file_a.txt": "content a", + "file_b.txt": "content b" + } + }), + ) + .await; + + fs.rename( + Path::new(path!("/root/src/file_a.txt")), + Path::new(path!("/root/src/new/renamed_a.txt")), + RenameOptions { + create_parents: true, + ..Default::default() + }, + ) + .await + .unwrap(); + + // Assert that the `file_a.txt` file was being renamed and moved to a + // different directory that did not exist before. + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/root/src/file_b.txt")), + PathBuf::from(path!("/root/src/new/renamed_a.txt")), + ] + ); + + let result = fs + .rename( + Path::new(path!("/root/src/file_b.txt")), + Path::new(path!("/root/src/old/renamed_b.txt")), + RenameOptions { + create_parents: false, + ..Default::default() + }, + ) + .await; + + // Assert that the `file_b.txt` file was not renamed nor moved, as + // `create_parents` was set to `false`. + // different directory that did not exist before. + assert!(result.is_err()); + assert_eq!( + fs.files(), + vec![ + PathBuf::from(path!("/root/src/file_b.txt")), + PathBuf::from(path!("/root/src/new/renamed_a.txt")), + ] + ); +} + +#[gpui::test] +#[cfg(unix)] +async fn test_realfs_broken_symlink_metadata(executor: BackgroundExecutor) { + let tempdir = TempDir::new().unwrap(); + let path = tempdir.path(); + let fs = RealFs::new(None, executor); + let symlink_path = path.join("symlink"); + smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("file_a.txt"))).unwrap(); + let metadata = fs + .metadata(&symlink_path) + .await + .expect("metadata call succeeds") + .expect("metadata returned"); + assert!(metadata.is_symlink); + assert!(!metadata.is_dir); + assert!(!metadata.is_fifo); + assert!(!metadata.is_executable); + // don't care about len or mtime on symlinks? +} + +#[gpui::test] +#[cfg(unix)] +async fn test_realfs_symlink_loop_metadata(executor: BackgroundExecutor) { + let tempdir = TempDir::new().unwrap(); + let path = tempdir.path(); + let fs = RealFs::new(None, executor); + let symlink_path = path.join("symlink"); + smol::block_on(fs.create_symlink(&symlink_path, PathBuf::from("symlink"))).unwrap(); + let metadata = fs + .metadata(&symlink_path) + .await + .expect("metadata call succeeds") + .expect("metadata returned"); + assert!(metadata.is_symlink); + assert!(!metadata.is_dir); + assert!(!metadata.is_fifo); + assert!(!metadata.is_executable); + // don't care about len or mtime on symlinks? +} diff --git a/crates/fs/tests/integration/main.rs b/crates/fs/tests/integration/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..7608563d7df145840046973c7af37d105954125f --- /dev/null +++ b/crates/fs/tests/integration/main.rs @@ -0,0 +1,2 @@ +mod fake_git_repo; +mod fs;