mirror of
https://github.com/zed-industries/zed.git
synced 2024-10-28 01:07:09 +00:00
Start on a randomized test for Worktree
This commit is contained in:
parent
17f2df3e71
commit
ca62d01b53
1 changed files with 280 additions and 64 deletions
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue