mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 14:01:37 +00:00
- Completed the implementation of roundtrip tests to validate the syncback process, ensuring that building a project, syncing back, and rebuilding produces semantically identical outputs. - Created a new utility module for roundtrip testing, including functions for running builds and asserting directory equality. - Developed clean mode stress tests to verify that syncing back to a dirty directory yields identical results to syncing back to a fresh directory, covering various mutation scenarios. - Updated existing test modules to incorporate new test cases and ensure consistency in handling standalone scripts transitioning to directory format when children are added.
225 lines
8 KiB
Rust
225 lines
8 KiB
Rust
use std::{
|
|
collections::{HashMap, HashSet},
|
|
io,
|
|
path::{Path, PathBuf},
|
|
};
|
|
|
|
use memofs::Vfs;
|
|
|
|
/// A simple representation of a subsection of a file system.
|
|
#[derive(Default)]
|
|
pub struct FsSnapshot {
|
|
/// Paths representing new files mapped to their contents.
|
|
added_files: HashMap<PathBuf, Vec<u8>>,
|
|
/// Paths representing new directories.
|
|
added_dirs: HashSet<PathBuf>,
|
|
/// Paths representing removed files.
|
|
removed_files: HashSet<PathBuf>,
|
|
/// Paths representing removed directories.
|
|
removed_dirs: HashSet<PathBuf>,
|
|
}
|
|
|
|
impl FsSnapshot {
|
|
/// Creates a new `FsSnapshot`.
|
|
pub fn new() -> Self {
|
|
Self {
|
|
added_files: HashMap::new(),
|
|
added_dirs: HashSet::new(),
|
|
removed_files: HashSet::new(),
|
|
removed_dirs: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
/// Adds the given path to the `FsSnapshot` as a file with the given
|
|
/// contents, then returns it.
|
|
pub fn with_added_file<P: AsRef<Path>>(mut self, path: P, data: Vec<u8>) -> Self {
|
|
self.added_files.insert(path.as_ref().to_path_buf(), data);
|
|
self
|
|
}
|
|
|
|
/// Adds the given path to the `FsSnapshot` as a file with the given
|
|
/// then returns it.
|
|
pub fn with_added_dir<P: AsRef<Path>>(mut self, path: P) -> Self {
|
|
self.added_dirs.insert(path.as_ref().to_path_buf());
|
|
self
|
|
}
|
|
|
|
/// Merges two `FsSnapshot`s together.
|
|
#[inline]
|
|
pub fn merge(&mut self, other: Self) {
|
|
self.added_files.extend(other.added_files);
|
|
self.added_dirs.extend(other.added_dirs);
|
|
self.removed_files.extend(other.removed_files);
|
|
self.removed_dirs.extend(other.removed_dirs);
|
|
}
|
|
|
|
/// Merges two `FsSnapshot`s together, with a filter applied to the paths.
|
|
#[inline]
|
|
pub fn merge_with_filter<F>(&mut self, other: Self, mut predicate: F)
|
|
where
|
|
F: FnMut(&Path) -> bool,
|
|
{
|
|
self.added_files
|
|
.extend(other.added_files.into_iter().filter(|(k, _)| predicate(k)));
|
|
self.added_dirs
|
|
.extend(other.added_dirs.into_iter().filter(|p| predicate(p)));
|
|
self.removed_files
|
|
.extend(other.removed_files.into_iter().filter(|p| predicate(p)));
|
|
self.removed_dirs
|
|
.extend(other.removed_dirs.into_iter().filter(|p| predicate(p)));
|
|
}
|
|
|
|
/// Adds the provided path as a file with the given contents.
|
|
pub fn add_file<P: AsRef<Path>>(&mut self, path: P, data: Vec<u8>) {
|
|
self.added_files.insert(path.as_ref().to_path_buf(), data);
|
|
}
|
|
|
|
/// Adds the provided path as a directory.
|
|
pub fn add_dir<P: AsRef<Path>>(&mut self, path: P) {
|
|
self.added_dirs.insert(path.as_ref().to_path_buf());
|
|
}
|
|
|
|
/// Removes the provided path, as a file.
|
|
pub fn remove_file<P: AsRef<Path>>(&mut self, path: P) {
|
|
self.removed_files.insert(path.as_ref().to_path_buf());
|
|
}
|
|
|
|
/// Removes the provided path, as a directory.
|
|
pub fn remove_dir<P: AsRef<Path>>(&mut self, path: P) {
|
|
self.removed_dirs.insert(path.as_ref().to_path_buf());
|
|
}
|
|
|
|
/// Writes the `FsSnapshot` to the provided VFS, using the provided `base`
|
|
/// as a root for the other paths in the `FsSnapshot`.
|
|
///
|
|
/// This includes removals, but makes no effort to minimize work done.
|
|
pub fn write_to_vfs<P: AsRef<Path>>(&self, base: P, vfs: &Vfs) -> io::Result<()> {
|
|
let mut lock = vfs.lock();
|
|
|
|
let base_path = base.as_ref();
|
|
for dir_path in &self.added_dirs {
|
|
match lock.create_dir_all(base_path.join(dir_path)) {
|
|
Ok(_) => (),
|
|
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => (),
|
|
Err(err) => return Err(err),
|
|
};
|
|
}
|
|
for (path, contents) in &self.added_files {
|
|
lock.write(base_path.join(path), contents)?;
|
|
}
|
|
for dir_path in &self.removed_dirs {
|
|
lock.remove_dir_all(base_path.join(dir_path))?;
|
|
}
|
|
// Only remove files that aren't already inside a directory we're removing.
|
|
// remove_dir_all already deleted those files recursively.
|
|
// Also handle the case where the same file might be listed twice (absolute vs relative path)
|
|
// by gracefully handling "file not found" errors.
|
|
for path in &self.removed_files {
|
|
let is_inside_removed_dir = self
|
|
.removed_dirs
|
|
.iter()
|
|
.any(|dir| path.starts_with(dir));
|
|
if is_inside_removed_dir {
|
|
continue;
|
|
}
|
|
let full_path = base_path.join(path);
|
|
match lock.remove_file(&full_path) {
|
|
Ok(()) => (),
|
|
// File might have already been removed (e.g., by remove_dir_all, or listed twice
|
|
// with different path formats like absolute vs relative)
|
|
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
|
log::debug!(
|
|
"File already removed or doesn't exist: {}",
|
|
full_path.display()
|
|
);
|
|
}
|
|
Err(err) => return Err(err),
|
|
}
|
|
}
|
|
drop(lock);
|
|
|
|
log::debug!(
|
|
"Wrote {} directories and {} files to the file system",
|
|
self.added_dirs.len(),
|
|
self.added_files.len()
|
|
);
|
|
// Count how many files were skipped because they're inside removed directories
|
|
let files_inside_dirs = self
|
|
.removed_files
|
|
.iter()
|
|
.filter(|path| {
|
|
self.removed_dirs
|
|
.iter()
|
|
.any(|dir| path.starts_with(dir))
|
|
})
|
|
.count();
|
|
log::debug!(
|
|
"Removed {} directories and {} files from the file system ({} files skipped, inside removed dirs)",
|
|
self.removed_dirs.len(),
|
|
self.removed_files.len() - files_inside_dirs,
|
|
files_inside_dirs
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// Returns whether this `FsSnapshot` is empty or not.
|
|
#[inline]
|
|
pub fn is_empty(&self) -> bool {
|
|
self.added_files.is_empty()
|
|
&& self.added_dirs.is_empty()
|
|
&& self.removed_files.is_empty()
|
|
&& self.removed_dirs.is_empty()
|
|
}
|
|
|
|
/// Returns a list of paths that would be added by this `FsSnapshot`.
|
|
#[inline]
|
|
pub fn added_paths(&self) -> Vec<&Path> {
|
|
let mut list = Vec::with_capacity(self.added_files.len() + self.added_dirs.len());
|
|
list.extend(self.added_files());
|
|
list.extend(self.added_dirs());
|
|
|
|
list
|
|
}
|
|
|
|
/// Returns a list of paths that would be removed by this `FsSnapshot`.
|
|
#[inline]
|
|
pub fn removed_paths(&self) -> Vec<&Path> {
|
|
let mut list = Vec::with_capacity(self.removed_files.len() + self.removed_dirs.len());
|
|
list.extend(self.removed_files());
|
|
list.extend(self.removed_dirs());
|
|
|
|
list
|
|
}
|
|
|
|
/// Returns a list of file paths that would be added by this `FsSnapshot`
|
|
#[inline]
|
|
pub fn added_files(&self) -> Vec<&Path> {
|
|
let mut added_files: Vec<_> = self.added_files.keys().map(PathBuf::as_path).collect();
|
|
added_files.sort_unstable();
|
|
added_files
|
|
}
|
|
|
|
/// Returns a list of directory paths that would be added by this `FsSnapshot`
|
|
#[inline]
|
|
pub fn added_dirs(&self) -> Vec<&Path> {
|
|
let mut added_dirs: Vec<_> = self.added_dirs.iter().map(PathBuf::as_path).collect();
|
|
added_dirs.sort_unstable();
|
|
added_dirs
|
|
}
|
|
|
|
/// Returns a list of file paths that would be removed by this `FsSnapshot`
|
|
#[inline]
|
|
pub fn removed_files(&self) -> Vec<&Path> {
|
|
let mut removed_files: Vec<_> = self.removed_files.iter().map(PathBuf::as_path).collect();
|
|
removed_files.sort_unstable();
|
|
removed_files
|
|
}
|
|
|
|
/// Returns a list of directory paths that would be removed by this `FsSnapshot`
|
|
#[inline]
|
|
pub fn removed_dirs(&self) -> Vec<&Path> {
|
|
let mut removed_dirs: Vec<_> = self.removed_dirs.iter().map(PathBuf::as_path).collect();
|
|
removed_dirs.sort_unstable();
|
|
removed_dirs
|
|
}
|
|
}
|