Start on a randomized test for Worktree

This commit is contained in:
Antonio Scandurra 2021-04-19 12:01:33 +02:00
parent 17f2df3e71
commit ca62d01b53

View File

@ -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
}
}
}