diff --git a/Cargo.lock b/Cargo.lock index 8990500c56..ed799521d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4625,7 +4625,9 @@ dependencies = [ "client", "clock", "collections", + "ctor", "db", + "env_logger", "fs", "fsevent", "futures 0.3.25", diff --git a/crates/fs/src/fs.rs b/crates/fs/src/fs.rs index f640f35036..fd713ef3b5 100644 --- a/crates/fs/src/fs.rs +++ b/crates/fs/src/fs.rs @@ -380,6 +380,8 @@ struct FakeFsState { next_inode: u64, next_mtime: SystemTime, event_txs: Vec>>, + events_paused: bool, + buffered_events: Vec, } #[cfg(any(test, feature = "test-support"))] @@ -483,15 +485,21 @@ impl FakeFsState { I: IntoIterator, T: Into, { - let events = paths - .into_iter() - .map(|path| fsevent::Event { + self.buffered_events + .extend(paths.into_iter().map(|path| fsevent::Event { event_id: 0, flags: fsevent::StreamFlags::empty(), path: path.into(), - }) - .collect::>(); + })); + if !self.events_paused { + self.flush_events(self.buffered_events.len()); + } + } + + fn flush_events(&mut self, mut count: usize) { + count = count.min(self.buffered_events.len()); + let events = self.buffered_events.drain(0..count).collect::>(); self.event_txs.retain(|tx| { let _ = tx.try_send(events.clone()); !tx.is_closed() @@ -514,6 +522,8 @@ impl FakeFs { next_mtime: SystemTime::UNIX_EPOCH, next_inode: 1, event_txs: Default::default(), + buffered_events: Vec::new(), + events_paused: false, }), }) } @@ -567,6 +577,18 @@ impl FakeFs { state.emit_event(&[path]); } + pub async fn pause_events(&self) { + self.state.lock().await.events_paused = true; + } + + pub async fn buffered_event_count(&self) -> usize { + self.state.lock().await.buffered_events.len() + } + + pub async fn flush_events(&self, count: usize) { + self.state.lock().await.flush_events(count); + } + #[must_use] pub fn insert_tree<'a>( &'a self, @@ -868,7 +890,7 @@ impl Fs for FakeFs { .ok_or_else(|| anyhow!("cannot remove the root"))?; let base_name = path.file_name().unwrap(); - let state = self.state.lock().await; + let mut state = self.state.lock().await; let parent_entry = state.read_path(parent_path).await?; let mut parent_entry = parent_entry.lock().await; let entry = parent_entry @@ -892,7 +914,7 @@ impl Fs for FakeFs { e.remove(); } } - + state.emit_event(&[path]); Ok(()) } diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 89ed5563ab..be100b2e87 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -58,6 +58,8 @@ thiserror = "1.0.29" toml = "0.5" [dev-dependencies] +ctor = "0.1" +env_logger = "0.9" pretty_assertions = "1.3.0" client = { path = "../client", features = ["test-support"] } collections = { path = "../collections", features = ["test-support"] } diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 61f207c45a..383c1b5c47 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -14,6 +14,14 @@ use std::{cell::RefCell, os::unix, rc::Rc, task::Poll}; use unindent::Unindent as _; use util::{assert_set_eq, test::temp_tree}; +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} + #[gpui::test] async fn test_symlinks(cx: &mut gpui::TestAppContext) { let dir = temp_tree(json!({ diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 0d3e3b416b..7250de9a68 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2257,10 +2257,6 @@ impl BackgroundScanner { self.snapshot.lock().abs_path.clone() } - fn snapshot(&self) -> LocalSnapshot { - self.snapshot.lock().clone() - } - async fn run(mut self, events_rx: impl Stream>) { if self.notify.unbounded_send(ScanState::Initializing).is_err() { return; @@ -2657,8 +2653,7 @@ impl BackgroundScanner { } async fn update_ignore_statuses(&self) { - let mut snapshot = self.snapshot(); - + let mut snapshot = self.snapshot.lock().clone(); let mut ignores_to_update = Vec::new(); let mut ignores_to_delete = Vec::new(); for (parent_abs_path, (_, scan_id)) in &snapshot.ignores_by_parent_abs_path { @@ -3115,19 +3110,13 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { #[cfg(test)] mod tests { use super::*; - use anyhow::Result; use client::test::FakeHttpClient; use fs::repository::FakeGitRepository; use fs::{FakeFs, RealFs}; use gpui::{executor::Deterministic, TestAppContext}; use rand::prelude::*; use serde_json::json; - use std::{ - env, - fmt::Write, - time::{SystemTime, UNIX_EPOCH}, - }; - + use std::{env, fmt::Write}; use util::test::temp_tree; #[gpui::test] @@ -3572,7 +3561,7 @@ mod tests { } #[gpui::test(iterations = 100)] - fn test_random(mut rng: StdRng) { + async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) { let operations = env::var("OPERATIONS") .map(|o| o.parse().unwrap()) .unwrap_or(40); @@ -3580,90 +3569,80 @@ mod tests { .map(|o| o.parse().unwrap()) .unwrap_or(20); - let root_dir = tempdir::TempDir::new("worktree-test").unwrap(); + let root_dir = Path::new("/test"); + let fs = FakeFs::new(cx.background()) as Arc; + fs.as_fake().insert_tree(root_dir, json!({})).await; for _ in 0..initial_entries { - randomly_mutate_tree(root_dir.path(), 1.0, &mut rng).unwrap(); + randomly_mutate_tree(&fs, root_dir, 1.0, &mut rng).await; } - log::info!("Generated initial tree"); + log::info!("generated initial tree"); - let (notify_tx, _notify_rx) = mpsc::unbounded(); - let fs = Arc::new(RealFs); - let next_entry_id = Arc::new(AtomicUsize::new(0)); - let mut initial_snapshot = LocalSnapshot { - removed_entry_ids: Default::default(), - ignores_by_parent_abs_path: Default::default(), - git_repositories: Default::default(), - next_entry_id: next_entry_id.clone(), - snapshot: Snapshot { - id: WorktreeId::from_usize(0), - entries_by_path: Default::default(), - entries_by_id: Default::default(), - abs_path: root_dir.path().into(), - root_name: Default::default(), - root_char_bag: Default::default(), - scan_id: 0, - completed_scan_id: 0, - }, - }; - initial_snapshot.insert_entry( - Entry::new( - Path::new("").into(), - &smol::block_on(fs.metadata(root_dir.path())) - .unwrap() - .unwrap(), - &next_entry_id, - Default::default(), - ), - fs.as_ref(), - ); - let mut scanner = BackgroundScanner::new( - Arc::new(Mutex::new(initial_snapshot.clone())), - Arc::new(Mutex::new(HashMap::default())), - notify_tx, + let next_entry_id = Arc::new(AtomicUsize::default()); + let client = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx)); + let worktree = Worktree::local( + client.clone(), + root_dir, + true, fs.clone(), - Arc::new(gpui::executor::Background::new()), - ); - smol::block_on(scanner.scan_dirs()).unwrap(); - scanner.snapshot().check_invariants(); + next_entry_id.clone(), + &mut cx.to_async(), + ) + .await + .unwrap(); + + worktree + .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) + .await; - let mut events = Vec::new(); let mut snapshots = Vec::new(); let mut mutations_len = operations; while mutations_len > 1 { - if !events.is_empty() && rng.gen_bool(0.4) { - let len = rng.gen_range(0..=events.len()); - let to_deliver = events.drain(0..len).collect::>(); - log::info!("Delivering events: {:#?}", to_deliver); - smol::block_on(scanner.process_events(to_deliver, false)); - scanner.snapshot().check_invariants(); + randomly_mutate_tree(&fs, root_dir, 1.0, &mut rng).await; + let buffered_event_count = fs.as_fake().buffered_event_count().await; + if buffered_event_count > 0 && rng.gen_bool(0.3) { + let len = rng.gen_range(0..=buffered_event_count); + log::info!("flushing {} events", len); + fs.as_fake().flush_events(len).await; } else { - events.extend(randomly_mutate_tree(root_dir.path(), 0.6, &mut rng).unwrap()); + randomly_mutate_tree(&fs, root_dir, 0.6, &mut rng).await; mutations_len -= 1; } + cx.foreground().run_until_parked(); if rng.gen_bool(0.2) { - snapshots.push(scanner.snapshot()); + log::info!("storing snapshot {}", snapshots.len()); + let snapshot = + worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); + snapshots.push(snapshot); } } - log::info!("Quiescing: {:#?}", events); - smol::block_on(scanner.process_events(events, false)); - scanner.snapshot().check_invariants(); - let (notify_tx, _notify_rx) = mpsc::unbounded(); - let mut new_scanner = BackgroundScanner::new( - Arc::new(Mutex::new(initial_snapshot)), - Arc::new(Mutex::new(HashMap::default())), - notify_tx, - scanner.fs.clone(), - scanner.executor.clone(), - ); - smol::block_on(new_scanner.scan_dirs()).unwrap(); - assert_eq!( - scanner.snapshot().to_vec(true), - new_scanner.snapshot().to_vec(true) - ); + log::info!("quiescing"); + fs.as_fake().flush_events(usize::MAX).await; + cx.foreground().run_until_parked(); + let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); + snapshot.check_invariants(); - for mut prev_snapshot in snapshots { + { + let new_worktree = Worktree::local( + client.clone(), + root_dir, + true, + fs.clone(), + next_entry_id, + &mut cx.to_async(), + ) + .await + .unwrap(); + new_worktree + .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) + .await; + let new_snapshot = + new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); + assert_eq!(snapshot.to_vec(true), new_snapshot.to_vec(true)); + } + + for (i, mut prev_snapshot) in snapshots.into_iter().enumerate() { let include_ignored = rng.gen::(); if !include_ignored { let mut entries_by_path_edits = Vec::new(); @@ -3683,54 +3662,66 @@ mod tests { prev_snapshot.entries_by_id.edit(entries_by_id_edits, &()); } - let update = scanner - .snapshot() - .build_update(&prev_snapshot, 0, 0, include_ignored); - prev_snapshot.apply_remote_update(update).unwrap(); + let update = snapshot.build_update(&prev_snapshot, 0, 0, include_ignored); + prev_snapshot.apply_remote_update(update.clone()).unwrap(); assert_eq!( - prev_snapshot.to_vec(true), - scanner.snapshot().to_vec(include_ignored) + prev_snapshot.to_vec(include_ignored), + snapshot.to_vec(include_ignored), + "wrong update for snapshot {i}. update: {:?}", + update ); } } - fn randomly_mutate_tree( + async fn randomly_mutate_tree( + fs: &Arc, root_path: &Path, insertion_probability: f64, rng: &mut impl Rng, - ) -> Result> { - let root_path = root_path.canonicalize().unwrap(); - let (dirs, files) = read_dir_recursive(root_path.clone()); - - let mut events = Vec::new(); - let mut record_event = |path: PathBuf| { - events.push(fsevent::Event { - event_id: SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(), - flags: fsevent::StreamFlags::empty(), - path, - }); - }; + ) { + let mut files = Vec::new(); + let mut dirs = Vec::new(); + for path in fs.as_fake().paths().await { + if path.starts_with(root_path) { + if fs.is_file(&path).await { + files.push(path); + } else { + dirs.push(path); + } + } + } if (files.is_empty() && dirs.len() == 1) || rng.gen_bool(insertion_probability) { let path = dirs.choose(rng).unwrap(); let new_path = path.join(gen_name(rng)); if rng.gen() { - log::info!("Creating dir {:?}", new_path.strip_prefix(root_path)?); - std::fs::create_dir(&new_path)?; + log::info!( + "Creating dir {:?}", + new_path.strip_prefix(root_path).unwrap() + ); + fs.create_dir(&new_path).await.unwrap(); } else { - log::info!("Creating file {:?}", new_path.strip_prefix(root_path)?); - std::fs::write(&new_path, "")?; + log::info!( + "Creating file {:?}", + new_path.strip_prefix(root_path).unwrap() + ); + fs.create_file(&new_path, Default::default()).await.unwrap(); } - record_event(new_path); } else if rng.gen_bool(0.05) { let ignore_dir_path = dirs.choose(rng).unwrap(); let ignore_path = ignore_dir_path.join(&*GITIGNORE); - let (subdirs, subfiles) = read_dir_recursive(ignore_dir_path.clone()); + let subdirs = dirs + .iter() + .filter(|d| d.starts_with(&ignore_dir_path)) + .cloned() + .collect::>(); + let subfiles = files + .iter() + .filter(|d| d.starts_with(&ignore_dir_path)) + .cloned() + .collect::>(); let files_to_ignore = { let len = rng.gen_range(0..=subfiles.len()); subfiles.choose_multiple(rng, len) @@ -3746,7 +3737,8 @@ mod tests { ignore_contents, "{}", path_to_ignore - .strip_prefix(&ignore_dir_path)? + .strip_prefix(&ignore_dir_path) + .unwrap() .to_str() .unwrap() ) @@ -3754,11 +3746,16 @@ mod tests { } log::info!( "Creating {:?} with contents:\n{}", - ignore_path.strip_prefix(&root_path)?, + ignore_path.strip_prefix(&root_path).unwrap(), ignore_contents ); - std::fs::write(&ignore_path, ignore_contents).unwrap(); - record_event(ignore_path); + fs.save( + &ignore_path, + &ignore_contents.as_str().into(), + Default::default(), + ) + .await + .unwrap(); } else { let old_path = { let file_path = files.choose(rng); @@ -3777,7 +3774,15 @@ mod tests { let overwrite_existing_dir = !old_path.starts_with(&new_path_parent) && rng.gen_bool(0.3); let new_path = if overwrite_existing_dir { - std::fs::remove_dir_all(&new_path_parent).ok(); + fs.remove_dir( + &new_path_parent, + RemoveOptions { + recursive: true, + ignore_if_not_exists: true, + }, + ) + .await + .unwrap(); new_path_parent.to_path_buf() } else { new_path_parent.join(gen_name(rng)) @@ -3785,53 +3790,46 @@ mod tests { log::info!( "Renaming {:?} to {}{:?}", - old_path.strip_prefix(&root_path)?, + old_path.strip_prefix(&root_path).unwrap(), if overwrite_existing_dir { "overwrite " } else { "" }, - new_path.strip_prefix(&root_path)? + new_path.strip_prefix(&root_path).unwrap() ); - std::fs::rename(&old_path, &new_path)?; - record_event(old_path.clone()); - record_event(new_path); - } else if old_path.is_dir() { - let (dirs, files) = read_dir_recursive(old_path.clone()); - - log::info!("Deleting dir {:?}", old_path.strip_prefix(&root_path)?); - std::fs::remove_dir_all(&old_path).unwrap(); - for file in files { - record_event(file); - } - for dir in dirs { - record_event(dir); - } + fs.rename( + &old_path, + &new_path, + fs::RenameOptions { + overwrite: true, + ignore_if_exists: true, + }, + ) + .await + .unwrap(); + } else if fs.is_file(&old_path).await { + log::info!( + "Deleting file {:?}", + old_path.strip_prefix(&root_path).unwrap() + ); + fs.remove_file(old_path, Default::default()).await.unwrap(); } else { - log::info!("Deleting file {:?}", old_path.strip_prefix(&root_path)?); - std::fs::remove_file(old_path).unwrap(); - record_event(old_path.clone()); + log::info!( + "Deleting dir {:?}", + old_path.strip_prefix(&root_path).unwrap() + ); + fs.remove_dir( + &old_path, + RemoveOptions { + recursive: true, + ignore_if_not_exists: true, + }, + ) + .await + .unwrap(); } } - - Ok(events) - } - - fn read_dir_recursive(path: PathBuf) -> (Vec, Vec) { - let child_entries = std::fs::read_dir(&path).unwrap(); - let mut dirs = vec![path]; - let mut files = Vec::new(); - for child_entry in child_entries { - let child_path = child_entry.unwrap().path(); - if child_path.is_dir() { - let (child_dirs, child_files) = read_dir_recursive(child_path); - dirs.extend(child_dirs); - files.extend(child_files); - } else { - files.push(child_path); - } - } - (dirs, files) } fn gen_name(rng: &mut impl Rng) -> String {