mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-24 05:25:18 +00:00
We aren't making use of it in these crates and it unblocks some web-related work Release Notes: - N/A or Added/Fixed/Improved ...
914 lines
27 KiB
Rust
914 lines
27 KiB
Rust
use std::{
|
|
collections::BTreeSet,
|
|
ffi::OsString,
|
|
io::Write,
|
|
path::{Path, PathBuf},
|
|
time::Duration,
|
|
};
|
|
|
|
use futures::{FutureExt, StreamExt};
|
|
|
|
use fs::*;
|
|
use gpui::{BackgroundExecutor, TestAppContext};
|
|
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");
|
|
gpui::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");
|
|
gpui::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(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
|
async fn test_realfs_parallel_rename_without_overwrite_preserves_losing_source(
|
|
executor: BackgroundExecutor,
|
|
) {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let root = temp_dir.path();
|
|
let source_a = root.join("dir_a/shared.txt");
|
|
let source_b = root.join("dir_b/shared.txt");
|
|
let target = root.join("shared.txt");
|
|
|
|
std::fs::create_dir_all(source_a.parent().unwrap()).unwrap();
|
|
std::fs::create_dir_all(source_b.parent().unwrap()).unwrap();
|
|
std::fs::write(&source_a, "from a").unwrap();
|
|
std::fs::write(&source_b, "from b").unwrap();
|
|
|
|
let fs = RealFs::new(None, executor);
|
|
let (first_result, second_result) = futures::future::join(
|
|
fs.rename(&source_a, &target, RenameOptions::default()),
|
|
fs.rename(&source_b, &target, RenameOptions::default()),
|
|
)
|
|
.await;
|
|
|
|
assert_ne!(first_result.is_ok(), second_result.is_ok());
|
|
assert!(target.exists());
|
|
assert_eq!(source_a.exists() as u8 + source_b.exists() as u8, 1);
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
|
async fn test_realfs_rename_ignore_if_exists_leaves_source_and_target_unchanged(
|
|
executor: BackgroundExecutor,
|
|
) {
|
|
let temp_dir = TempDir::new().unwrap();
|
|
let root = temp_dir.path();
|
|
let source = root.join("source.txt");
|
|
let target = root.join("target.txt");
|
|
|
|
std::fs::write(&source, "from source").unwrap();
|
|
std::fs::write(&target, "from target").unwrap();
|
|
|
|
let fs = RealFs::new(None, executor);
|
|
let result = fs
|
|
.rename(
|
|
&source,
|
|
&target,
|
|
RenameOptions {
|
|
ignore_if_exists: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await;
|
|
|
|
assert!(result.is_ok());
|
|
|
|
assert_eq!(std::fs::read_to_string(&source).unwrap(), "from source");
|
|
assert_eq!(std::fs::read_to_string(&target).unwrap(), "from target");
|
|
}
|
|
|
|
#[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");
|
|
gpui::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");
|
|
gpui::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?
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_fake_fs_trash(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"src": {
|
|
"file_c.txt": "File C",
|
|
"file_d.txt": "File D"
|
|
},
|
|
"file_a.txt": "File A",
|
|
"file_b.txt": "File B",
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// Trashing a file.
|
|
let root_path = PathBuf::from(path!("/root"));
|
|
let path = path!("/root/file_a.txt").as_ref();
|
|
let trashed_entry = fs
|
|
.trash(path, Default::default())
|
|
.await
|
|
.expect("should be able to trash {path:?}");
|
|
|
|
assert_eq!(trashed_entry.name, "file_a.txt");
|
|
assert_eq!(trashed_entry.original_parent, root_path);
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/file_b.txt")),
|
|
PathBuf::from(path!("/root/src/file_c.txt")),
|
|
PathBuf::from(path!("/root/src/file_d.txt"))
|
|
]
|
|
);
|
|
|
|
let trash_entries = fs.trash_entries();
|
|
assert_eq!(trash_entries.len(), 1);
|
|
assert_eq!(trash_entries[0].name, "file_a.txt");
|
|
assert_eq!(trash_entries[0].original_parent, root_path);
|
|
|
|
// Trashing a directory.
|
|
let path = path!("/root/src").as_ref();
|
|
let trashed_entry = fs
|
|
.trash(
|
|
path,
|
|
RemoveOptions {
|
|
recursive: true,
|
|
..Default::default()
|
|
},
|
|
)
|
|
.await
|
|
.expect("should be able to trash {path:?}");
|
|
|
|
assert_eq!(trashed_entry.name, "src");
|
|
assert_eq!(trashed_entry.original_parent, root_path);
|
|
assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_b.txt"))]);
|
|
|
|
let trash_entries = fs.trash_entries();
|
|
assert_eq!(trash_entries.len(), 2);
|
|
assert_eq!(trash_entries[1].name, "src");
|
|
assert_eq!(trash_entries[1].original_parent, root_path);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_fake_fs_restore(executor: BackgroundExecutor) {
|
|
let fs = FakeFs::new(executor.clone());
|
|
fs.insert_tree(
|
|
path!("/root"),
|
|
json!({
|
|
"src": {
|
|
"file_a.txt": "File A",
|
|
"file_b.txt": "File B",
|
|
},
|
|
"file_c.txt": "File C",
|
|
}),
|
|
)
|
|
.await;
|
|
|
|
// Providing a non-existent `TrashedEntry` should result in an error.
|
|
let id = OsString::from("/trash/file_c.txt");
|
|
let name = OsString::from("file_c.txt");
|
|
let original_parent = PathBuf::from(path!("/root"));
|
|
let trashed_entry = TrashedEntry {
|
|
id,
|
|
name,
|
|
original_parent,
|
|
};
|
|
let result = fs.restore(trashed_entry).await;
|
|
assert!(matches!(result, Err(TrashRestoreError::NotFound { .. })));
|
|
|
|
// Attempt deleting a file, asserting that the filesystem no longer reports
|
|
// it as part of its list of files, restore it and verify that the list of
|
|
// files and trash has been updated accordingly.
|
|
let path = path!("/root/src/file_a.txt").as_ref();
|
|
let trashed_entry = fs.trash(path, Default::default()).await.unwrap();
|
|
|
|
assert_eq!(fs.trash_entries().len(), 1);
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/file_c.txt")),
|
|
PathBuf::from(path!("/root/src/file_b.txt"))
|
|
]
|
|
);
|
|
|
|
fs.restore(trashed_entry).await.unwrap();
|
|
|
|
assert_eq!(fs.trash_entries().len(), 0);
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/file_c.txt")),
|
|
PathBuf::from(path!("/root/src/file_a.txt")),
|
|
PathBuf::from(path!("/root/src/file_b.txt"))
|
|
]
|
|
);
|
|
|
|
// Deleting and restoring a directory should also remove all of its files
|
|
// but create a single trashed entry, which should be removed after
|
|
// restoration.
|
|
let options = RemoveOptions {
|
|
recursive: true,
|
|
..Default::default()
|
|
};
|
|
let path = path!("/root/src/").as_ref();
|
|
let trashed_entry = fs.trash(path, options).await.unwrap();
|
|
|
|
assert_eq!(fs.trash_entries().len(), 1);
|
|
assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
|
|
|
|
fs.restore(trashed_entry).await.unwrap();
|
|
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/file_c.txt")),
|
|
PathBuf::from(path!("/root/src/file_a.txt")),
|
|
PathBuf::from(path!("/root/src/file_b.txt"))
|
|
]
|
|
);
|
|
assert_eq!(fs.trash_entries().len(), 0);
|
|
|
|
// A collision error should be returned in case a file is being restored to
|
|
// a path where a file already exists.
|
|
let path = path!("/root/src/file_a.txt").as_ref();
|
|
let trashed_entry = fs.trash(path, Default::default()).await.unwrap();
|
|
|
|
assert_eq!(fs.trash_entries().len(), 1);
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/file_c.txt")),
|
|
PathBuf::from(path!("/root/src/file_b.txt"))
|
|
]
|
|
);
|
|
|
|
fs.write(path, "New File A".as_bytes()).await.unwrap();
|
|
|
|
assert_eq!(fs.trash_entries().len(), 1);
|
|
assert_eq!(
|
|
fs.files(),
|
|
vec![
|
|
PathBuf::from(path!("/root/file_c.txt")),
|
|
PathBuf::from(path!("/root/src/file_a.txt")),
|
|
PathBuf::from(path!("/root/src/file_b.txt"))
|
|
]
|
|
);
|
|
|
|
let file_contents = fs.files_with_contents(path);
|
|
assert!(fs.restore(trashed_entry).await.is_err());
|
|
assert_eq!(
|
|
file_contents,
|
|
vec![(PathBuf::from(path), b"New File A".to_vec())]
|
|
);
|
|
|
|
// A collision error should be returned in case a directory is being
|
|
// restored to a path where a directory already exists.
|
|
let options = RemoveOptions {
|
|
recursive: true,
|
|
..Default::default()
|
|
};
|
|
let path = path!("/root/src/").as_ref();
|
|
let trashed_entry = fs.trash(path, options).await.unwrap();
|
|
|
|
assert_eq!(fs.trash_entries().len(), 2);
|
|
assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
|
|
|
|
fs.create_dir(path).await.unwrap();
|
|
|
|
assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
|
|
assert_eq!(fs.trash_entries().len(), 2);
|
|
|
|
let result = fs.restore(trashed_entry).await;
|
|
assert!(result.is_err());
|
|
|
|
assert_eq!(fs.files(), vec![PathBuf::from(path!("/root/file_c.txt"))]);
|
|
assert_eq!(fs.trash_entries().len(), 2);
|
|
}
|
|
|
|
#[gpui::test]
|
|
#[ignore = "stress test; run explicitly when needed"]
|
|
async fn test_realfs_watch_stress_reports_missed_paths(
|
|
executor: BackgroundExecutor,
|
|
cx: &mut TestAppContext,
|
|
) {
|
|
const FILE_COUNT: usize = 32000;
|
|
cx.executor().allow_parking();
|
|
|
|
let fs = RealFs::new(None, executor.clone());
|
|
let temp_dir = TempDir::new().expect("create temp dir");
|
|
let root = temp_dir.path();
|
|
|
|
let mut file_paths = Vec::with_capacity(FILE_COUNT);
|
|
let mut expected_paths = BTreeSet::new();
|
|
|
|
for index in 0..FILE_COUNT {
|
|
let dir_path = root.join(format!("dir-{index:04}"));
|
|
let file_path = dir_path.join("file.txt");
|
|
fs.create_dir(&dir_path).await.expect("create watched dir");
|
|
fs.write(&file_path, b"before")
|
|
.await
|
|
.expect("create initial file");
|
|
expected_paths.insert(file_path.clone());
|
|
file_paths.push(file_path);
|
|
}
|
|
|
|
let (mut events, watcher) = fs.watch(root, Duration::from_millis(10)).await;
|
|
let _watcher = watcher;
|
|
|
|
for file_path in &expected_paths {
|
|
_watcher
|
|
.add(file_path.parent().expect("file has parent"))
|
|
.expect("add explicit directory watch");
|
|
}
|
|
|
|
for (index, file_path) in file_paths.iter().enumerate() {
|
|
let content = format!("after-{index}");
|
|
fs.write(file_path, content.as_bytes())
|
|
.await
|
|
.expect("modify watched file");
|
|
}
|
|
|
|
let mut changed_paths = BTreeSet::new();
|
|
let mut rescan_count: u32 = 0;
|
|
let timeout = executor.timer(Duration::from_secs(10)).fuse();
|
|
|
|
futures::pin_mut!(timeout);
|
|
|
|
let mut ticks = 0;
|
|
while ticks < 1000 {
|
|
if let Some(batch) = events.next().fuse().now_or_never().flatten() {
|
|
for event in batch {
|
|
if event.kind == Some(PathEventKind::Rescan) {
|
|
rescan_count += 1;
|
|
}
|
|
if expected_paths.contains(&event.path) {
|
|
changed_paths.insert(event.path);
|
|
}
|
|
}
|
|
if changed_paths.len() == expected_paths.len() {
|
|
break;
|
|
}
|
|
ticks = 0;
|
|
} else {
|
|
ticks += 1;
|
|
executor.timer(Duration::from_millis(10)).await;
|
|
}
|
|
}
|
|
|
|
let missed_paths: BTreeSet<_> = expected_paths.difference(&changed_paths).cloned().collect();
|
|
|
|
eprintln!(
|
|
"realfs watch stress: expected={}, observed={}, missed={}, rescan={}",
|
|
expected_paths.len(),
|
|
changed_paths.len(),
|
|
missed_paths.len(),
|
|
rescan_count
|
|
);
|
|
|
|
assert!(
|
|
missed_paths.is_empty() || rescan_count > 0,
|
|
"missed {} paths without rescan being reported",
|
|
missed_paths.len()
|
|
);
|
|
}
|