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,
|
ffi::OsStr,
|
||||||
fmt, fs,
|
fmt, fs,
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
ops::AddAssign,
|
ops::{AddAssign, Deref},
|
||||||
os::unix::fs::MetadataExt,
|
os::unix::fs::MetadataExt,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -40,14 +40,6 @@ pub struct Worktree {
|
||||||
poll_scheduled: bool,
|
poll_scheduled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Snapshot {
|
|
||||||
id: usize,
|
|
||||||
path: Arc<Path>,
|
|
||||||
root_inode: Option<u64>,
|
|
||||||
entries: SumTree<Entry>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct FileHandle {
|
pub struct FileHandle {
|
||||||
worktree: ModelHandle<Worktree>,
|
worktree: ModelHandle<Worktree>,
|
||||||
|
@ -129,31 +121,6 @@ impl Worktree {
|
||||||
Ok(result)
|
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(
|
pub fn load_history(
|
||||||
&self,
|
&self,
|
||||||
ino: u64,
|
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)]
|
#[cfg(test)]
|
||||||
pub fn files<'a>(&'a self) -> impl Iterator<Item = u64> + 'a {
|
pub fn files<'a>(&'a self) -> impl Iterator<Item = u64> + 'a {
|
||||||
self.snapshot
|
self.snapshot
|
||||||
|
@ -231,14 +173,26 @@ impl Entity for Worktree {
|
||||||
type Event = ();
|
type Event = ();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Deref for Worktree {
|
||||||
|
type Target = Snapshot;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.snapshot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Debug for Worktree {
|
impl fmt::Debug for Worktree {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
if let Some(root_ino) = self.snapshot.root_inode {
|
self.snapshot.fmt(f)
|
||||||
self.fmt_entry(f, root_ino, 0)
|
|
||||||
} else {
|
|
||||||
write!(f, "Empty tree\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Snapshot {
|
||||||
|
id: usize,
|
||||||
|
path: Arc<Path>,
|
||||||
|
root_inode: Option<u64>,
|
||||||
|
entries: SumTree<Entry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Snapshot {
|
impl Snapshot {
|
||||||
|
@ -274,6 +228,30 @@ impl Snapshot {
|
||||||
.and_then(|inode| self.entries.get(&inode))
|
.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(
|
fn reparent_entry(
|
||||||
&mut self,
|
&mut self,
|
||||||
child_inode: u64,
|
child_inode: u64,
|
||||||
|
@ -351,6 +329,41 @@ impl Snapshot {
|
||||||
|
|
||||||
self.entries.edit(insertions);
|
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 {
|
impl FileHandle {
|
||||||
|
@ -987,8 +1000,13 @@ mod tests {
|
||||||
use crate::test::*;
|
use crate::test::*;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::App;
|
use gpui::App;
|
||||||
|
use log::LevelFilter;
|
||||||
|
use rand::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use simplelog::SimpleLogger;
|
||||||
|
use std::env;
|
||||||
use std::os::unix;
|
use std::os::unix;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_populate_and_search() {
|
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…
Add table
Add a link
Reference in a new issue