Start on a randomized test for `Worktree`

Antonio Scandurra created

Change summary

zed/src/worktree.rs | 344 ++++++++++++++++++++++++++++++++++++++--------
1 file changed, 280 insertions(+), 64 deletions(-)

Detailed changes

zed/src/worktree.rs 🔗

@@ -17,7 +17,7 @@ use std::{
     ffi::OsStr,
     fmt, fs,
     io::{self, Read, Write},
-    ops::AddAssign,
+    ops::{AddAssign, Deref},
     os::unix::fs::MetadataExt,
     path::{Path, PathBuf},
     sync::Arc,
@@ -40,14 +40,6 @@ pub struct Worktree {
     poll_scheduled: bool,
 }
 
-#[derive(Clone)]
-pub struct Snapshot {
-    id: usize,
-    path: Arc<Path>,
-    root_inode: Option<u64>,
-    entries: SumTree<Entry>,
-}
-
 #[derive(Clone)]
 pub struct FileHandle {
     worktree: ModelHandle<Worktree>,
@@ -129,31 +121,6 @@ impl Worktree {
         Ok(result)
     }
 
-    pub fn path_for_inode(&self, ino: u64, include_root: bool) -> Result<PathBuf> {
-        let mut components = Vec::new();
-        let mut entry = self
-            .snapshot
-            .entries
-            .get(&ino)
-            .ok_or_else(|| anyhow!("entry does not exist in worktree"))?;
-        components.push(entry.name());
-        while let Some(parent) = entry.parent() {
-            entry = self.snapshot.entries.get(&parent).unwrap();
-            components.push(entry.name());
-        }
-
-        let mut components = components.into_iter().rev();
-        if !include_root {
-            components.next();
-        }
-
-        let mut path = PathBuf::new();
-        for component in components {
-            path.push(component);
-        }
-        Ok(path)
-    }
-
     pub fn load_history(
         &self,
         ino: u64,
@@ -187,31 +154,6 @@ impl Worktree {
         })
     }
 
-    fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result {
-        match self.snapshot.entries.get(&ino).unwrap() {
-            Entry::Dir { name, children, .. } => {
-                write!(
-                    f,
-                    "{}{}/ ({})\n",
-                    " ".repeat(indent),
-                    name.to_string_lossy(),
-                    ino
-                )?;
-                for child_id in children.iter() {
-                    self.fmt_entry(f, *child_id, indent + 2)?;
-                }
-                Ok(())
-            }
-            Entry::File { name, .. } => write!(
-                f,
-                "{}{} ({})\n",
-                " ".repeat(indent),
-                name.to_string_lossy(),
-                ino
-            ),
-        }
-    }
-
     #[cfg(test)]
     pub fn files<'a>(&'a self) -> impl Iterator<Item = u64> + 'a {
         self.snapshot
@@ -231,16 +173,28 @@ impl Entity for Worktree {
     type Event = ();
 }
 
+impl Deref for Worktree {
+    type Target = Snapshot;
+
+    fn deref(&self) -> &Self::Target {
+        &self.snapshot
+    }
+}
+
 impl fmt::Debug for Worktree {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        if let Some(root_ino) = self.snapshot.root_inode {
-            self.fmt_entry(f, root_ino, 0)
-        } else {
-            write!(f, "Empty tree\n")
-        }
+        self.snapshot.fmt(f)
     }
 }
 
+#[derive(Clone)]
+pub struct Snapshot {
+    id: usize,
+    path: Arc<Path>,
+    root_inode: Option<u64>,
+    entries: SumTree<Entry>,
+}
+
 impl Snapshot {
     pub fn file_count(&self) -> usize {
         self.entries.summary().file_count
@@ -274,6 +228,30 @@ impl Snapshot {
             .and_then(|inode| self.entries.get(&inode))
     }
 
+    pub fn path_for_inode(&self, ino: u64, include_root: bool) -> Result<PathBuf> {
+        let mut components = Vec::new();
+        let mut entry = self
+            .entries
+            .get(&ino)
+            .ok_or_else(|| anyhow!("entry does not exist in worktree"))?;
+        components.push(entry.name());
+        while let Some(parent) = entry.parent() {
+            entry = self.entries.get(&parent).unwrap();
+            components.push(entry.name());
+        }
+
+        let mut components = components.into_iter().rev();
+        if !include_root {
+            components.next();
+        }
+
+        let mut path = PathBuf::new();
+        for component in components {
+            path.push(component);
+        }
+        Ok(path)
+    }
+
     fn reparent_entry(
         &mut self,
         child_inode: u64,
@@ -351,6 +329,41 @@ impl Snapshot {
 
         self.entries.edit(insertions);
     }
+
+    fn fmt_entry(&self, f: &mut fmt::Formatter<'_>, ino: u64, indent: usize) -> fmt::Result {
+        match self.entries.get(&ino).unwrap() {
+            Entry::Dir { name, children, .. } => {
+                write!(
+                    f,
+                    "{}{}/ ({})\n",
+                    " ".repeat(indent),
+                    name.to_string_lossy(),
+                    ino
+                )?;
+                for child_id in children.iter() {
+                    self.fmt_entry(f, *child_id, indent + 2)?;
+                }
+                Ok(())
+            }
+            Entry::File { name, .. } => write!(
+                f,
+                "{}{} ({})\n",
+                " ".repeat(indent),
+                name.to_string_lossy(),
+                ino
+            ),
+        }
+    }
+}
+
+impl fmt::Debug for Snapshot {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if let Some(root_ino) = self.root_inode {
+            self.fmt_entry(f, root_ino, 0)
+        } else {
+            write!(f, "Empty tree\n")
+        }
+    }
 }
 
 impl FileHandle {
@@ -987,8 +1000,13 @@ mod tests {
     use crate::test::*;
     use anyhow::Result;
     use gpui::App;
+    use log::LevelFilter;
+    use rand::prelude::*;
     use serde_json::json;
+    use simplelog::SimpleLogger;
+    use std::env;
     use std::os::unix;
+    use std::time::{SystemTime, UNIX_EPOCH};
 
     #[test]
     fn test_populate_and_search() {
@@ -1136,4 +1154,202 @@ mod tests {
             });
         });
     }
+
+    #[test]
+    fn test_random() {
+        if let Ok(true) = env::var("LOG").map(|l| l.parse().unwrap()) {
+            SimpleLogger::init(LevelFilter::Info, Default::default()).unwrap();
+        }
+
+        let iterations = env::var("ITERATIONS")
+            .map(|i| i.parse().unwrap())
+            .unwrap_or(100);
+        let operations = env::var("OPERATIONS")
+            .map(|o| o.parse().unwrap())
+            .unwrap_or(40);
+        let seeds = if let Ok(seed) = env::var("SEED").map(|s| s.parse().unwrap()) {
+            seed..seed + 1
+        } else {
+            0..iterations
+        };
+
+        for seed in seeds {
+            dbg!(seed);
+            let mut rng = StdRng::seed_from_u64(seed);
+
+            let root_dir = tempdir::TempDir::new(&format!("test-{}", seed)).unwrap();
+            for _ in 0..20 {
+                randomly_mutate_tree(root_dir.path(), 1.0, &mut rng).unwrap();
+            }
+            log::info!("Generated initial tree");
+
+            let (notify_tx, _notify_rx) = smol::channel::unbounded();
+            let scanner = BackgroundScanner::new(
+                Snapshot {
+                    id: 0,
+                    path: root_dir.path().into(),
+                    root_inode: None,
+                    entries: Default::default(),
+                },
+                notify_tx,
+            );
+            scanner.scan_dirs().unwrap();
+
+            let mut events = 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::<Vec<_>>();
+                    scanner.process_events(to_deliver);
+                } else {
+                    events.extend(randomly_mutate_tree(root_dir.path(), 0.6, &mut rng).unwrap());
+                    mutations_len -= 1;
+                }
+            }
+            scanner.process_events(events);
+
+            let (notify_tx, _notify_rx) = smol::channel::unbounded();
+            let new_scanner = BackgroundScanner::new(
+                Snapshot {
+                    id: 0,
+                    path: root_dir.path().into(),
+                    root_inode: None,
+                    entries: Default::default(),
+                },
+                notify_tx,
+            );
+            new_scanner.scan_dirs().unwrap();
+            assert_eq!(scanner.snapshot().to_vec(), new_scanner.snapshot().to_vec());
+        }
+    }
+
+    fn randomly_mutate_tree(
+        root_path: &Path,
+        insertion_probability: f64,
+        rng: &mut impl Rng,
+    ) -> Result<Vec<fsevent::Event>> {
+        let (dirs, files) = read_dir_recursive(root_path.to_path_buf());
+
+        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,
+            });
+        };
+
+        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)?);
+                fs::create_dir(&new_path)?;
+            } else {
+                log::info!("Creating file {:?}", new_path.strip_prefix(root_path)?);
+                fs::write(&new_path, "")?;
+            }
+            record_event(new_path);
+        } else {
+            let old_path = {
+                let file_path = files.choose(rng);
+                let dir_path = dirs[1..].choose(rng);
+                file_path.into_iter().chain(dir_path).choose(rng).unwrap()
+            };
+
+            let is_rename = rng.gen();
+            if is_rename {
+                let new_path_parent = dirs
+                    .iter()
+                    .filter(|d| !d.starts_with(old_path))
+                    .choose(rng)
+                    .unwrap();
+                let new_path = new_path_parent.join(gen_name(rng));
+
+                log::info!(
+                    "Renaming {:?} to {:?}",
+                    old_path.strip_prefix(&root_path)?,
+                    new_path.strip_prefix(&root_path)?
+                );
+                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)?);
+                fs::remove_dir_all(&old_path).unwrap();
+                for file in files {
+                    record_event(file);
+                }
+                for dir in dirs {
+                    record_event(dir);
+                }
+            } else {
+                log::info!("Deleting file {:?}", old_path.strip_prefix(&root_path)?);
+                fs::remove_file(old_path).unwrap();
+                record_event(old_path.clone());
+            }
+        }
+
+        Ok(events)
+    }
+
+    fn read_dir_recursive(path: PathBuf) -> (Vec<PathBuf>, Vec<PathBuf>) {
+        let child_entries = 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 {
+        (0..6)
+            .map(|_| rng.sample(rand::distributions::Alphanumeric))
+            .map(char::from)
+            .collect()
+    }
+
+    impl Snapshot {
+        fn to_vec(&self) -> Vec<(PathBuf, u64)> {
+            use std::iter::FromIterator;
+
+            let mut paths = Vec::new();
+
+            let mut stack = Vec::new();
+            stack.extend(self.root_inode);
+            while let Some(inode) = stack.pop() {
+                let computed_path = self.path_for_inode(inode, true).unwrap();
+                match self.entries.get(&inode).unwrap() {
+                    Entry::Dir { children, .. } => {
+                        stack.extend_from_slice(children);
+                    }
+                    Entry::File { path, .. } => {
+                        assert_eq!(
+                            String::from_iter(path.path.iter()),
+                            computed_path.to_str().unwrap()
+                        );
+                    }
+                }
+                paths.push((computed_path, inode));
+            }
+
+            paths.sort_by(|a, b| a.0.cmp(&b.0));
+            paths
+        }
+    }
 }