mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Add comprehensive syncback roundtrip and clean mode stress tests
- 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.
This commit is contained in:
parent
17d250f26a
commit
4a66eff938
10 changed files with 1517 additions and 50 deletions
|
|
@ -110,8 +110,31 @@ impl FsSnapshot {
|
|||
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 {
|
||||
lock.remove_file(base_path.join(path))?;
|
||||
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);
|
||||
|
||||
|
|
@ -120,10 +143,21 @@ impl FsSnapshot {
|
|||
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",
|
||||
"Removed {} directories and {} files from the file system ({} files skipped, inside removed dirs)",
|
||||
self.removed_dirs.len(),
|
||||
self.removed_files.len()
|
||||
self.removed_files.len() - files_inside_dirs,
|
||||
files_inside_dirs
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -221,7 +221,9 @@ pub fn syncback_loop_with_stats(
|
|||
// not just what Rojo loaded into its tree. This ensures orphaned files
|
||||
// (like duplicates Rojo couldn't load) get cleaned up properly.
|
||||
// Clean mode is essentially: "delete everything and rebuild from Studio"
|
||||
eprintln!("[DEBUG] incremental = {}", incremental);
|
||||
let existing_paths: HashSet<PathBuf> = if !incremental {
|
||||
eprintln!("[DEBUG] Clean mode: scanning filesystem for existing paths");
|
||||
log::debug!("Clean mode: scanning filesystem for existing paths");
|
||||
let mut paths = HashSet::new();
|
||||
|
||||
|
|
@ -246,9 +248,13 @@ pub fn syncback_loop_with_stats(
|
|||
ignore_patterns: &Option<Vec<Glob>>,
|
||||
project_path: &Path,
|
||||
) {
|
||||
eprintln!("[DEBUG] scan_directory: {}", dir.display());
|
||||
let entries = match std::fs::read_dir(dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return,
|
||||
Err(e) => {
|
||||
eprintln!("[DEBUG] Failed to read directory {}: {}", dir.display(), e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
|
|
@ -256,16 +262,19 @@ pub fn syncback_loop_with_stats(
|
|||
|
||||
// Skip paths matching ignore patterns
|
||||
if !is_valid_path(ignore_patterns, project_path, &path) {
|
||||
eprintln!("[DEBUG] Skipping {} (matches ignore pattern)", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip hidden files/directories (starting with .)
|
||||
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
|
||||
if name.starts_with('.') {
|
||||
eprintln!("[DEBUG] Skipping {} (hidden)", path.display());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!("[DEBUG] Found: {} (is_dir: {})", path.display(), path.is_dir());
|
||||
paths.insert(path.clone());
|
||||
|
||||
if path.is_dir() {
|
||||
|
|
@ -274,18 +283,33 @@ pub fn syncback_loop_with_stats(
|
|||
}
|
||||
}
|
||||
|
||||
eprintln!(
|
||||
"[DEBUG] dirs_to_scan: {:?}",
|
||||
dirs_to_scan.iter().map(|p| p.display().to_string()).collect::<Vec<_>>()
|
||||
);
|
||||
|
||||
for dir in &dirs_to_scan {
|
||||
if dir.is_dir() {
|
||||
eprintln!("[DEBUG] Scanning directory: {}", dir.display());
|
||||
scan_directory(dir, &mut paths, &ignore_patterns, project_path);
|
||||
} else if dir.exists() {
|
||||
// It's a file, add it directly
|
||||
eprintln!("[DEBUG] Adding file directly: {}", dir.display());
|
||||
if is_valid_path(&ignore_patterns, project_path, dir) {
|
||||
paths.insert(dir.clone());
|
||||
}
|
||||
} else {
|
||||
eprintln!("[DEBUG] Path does not exist: {}", dir.display());
|
||||
}
|
||||
}
|
||||
|
||||
log::debug!("Scanned {} existing paths from filesystem", paths.len());
|
||||
eprintln!(
|
||||
"[DEBUG] Scanned {} existing paths from filesystem",
|
||||
paths.len()
|
||||
);
|
||||
for p in &paths {
|
||||
eprintln!("[DEBUG] - {}", p.display());
|
||||
}
|
||||
paths
|
||||
} else {
|
||||
HashSet::new()
|
||||
|
|
|
|||
|
|
@ -996,11 +996,21 @@ impl ApiService {
|
|||
true
|
||||
}
|
||||
ExistingFileFormat::Standalone => {
|
||||
log::debug!(
|
||||
"Syncback: Preserving existing standalone format for {}",
|
||||
added.name
|
||||
);
|
||||
false
|
||||
// Standalone format can stay standalone only if there are no children.
|
||||
// If children were added in Studio, we must convert to directory format.
|
||||
if has_children {
|
||||
log::info!(
|
||||
"Syncback: Converting {} from standalone to directory format (children added)",
|
||||
added.name
|
||||
);
|
||||
true
|
||||
} else {
|
||||
log::debug!(
|
||||
"Syncback: Preserving existing standalone format for {}",
|
||||
added.name
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
ExistingFileFormat::None => {
|
||||
// New instance - use has_children to decide
|
||||
|
|
@ -1010,8 +1020,10 @@ impl ApiService {
|
|||
|
||||
if use_directory {
|
||||
// Directory format: Name/init.luau
|
||||
// Only remove standalone if this is a new instance transitioning to directory
|
||||
if existing_format == ExistingFileFormat::None {
|
||||
// Remove standalone file if transitioning to directory (new instance or format conversion)
|
||||
if existing_format == ExistingFileFormat::None
|
||||
|| existing_format == ExistingFileFormat::Standalone
|
||||
{
|
||||
let standalone_path = parent_dir.join(format!("{}.luau", added.name));
|
||||
if standalone_path.exists() {
|
||||
if let Err(err) = fs::remove_file(&standalone_path) {
|
||||
|
|
@ -1090,18 +1102,30 @@ impl ApiService {
|
|||
true
|
||||
}
|
||||
ExistingFileFormat::Standalone => {
|
||||
log::debug!(
|
||||
"Syncback: Preserving existing standalone format for Script {}",
|
||||
added.name
|
||||
);
|
||||
false
|
||||
// Standalone format can stay standalone only if there are no children.
|
||||
// If children were added in Studio, we must convert to directory format.
|
||||
if has_children {
|
||||
log::info!(
|
||||
"Syncback: Converting Script {} from standalone to directory format (children added)",
|
||||
added.name
|
||||
);
|
||||
true
|
||||
} else {
|
||||
log::debug!(
|
||||
"Syncback: Preserving existing standalone format for Script {}",
|
||||
added.name
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
ExistingFileFormat::None => has_children,
|
||||
};
|
||||
|
||||
if use_directory {
|
||||
// Only clean up if this is a new instance
|
||||
if existing_format == ExistingFileFormat::None {
|
||||
// Clean up standalone files if transitioning to directory (new instance or format conversion)
|
||||
if existing_format == ExistingFileFormat::None
|
||||
|| existing_format == ExistingFileFormat::Standalone
|
||||
{
|
||||
for suffix in &["server", "client"] {
|
||||
let standalone_path =
|
||||
parent_dir.join(format!("{}.{}.luau", added.name, suffix));
|
||||
|
|
@ -1159,18 +1183,30 @@ impl ApiService {
|
|||
true
|
||||
}
|
||||
ExistingFileFormat::Standalone => {
|
||||
log::debug!(
|
||||
"Syncback: Preserving existing standalone format for LocalScript {}",
|
||||
added.name
|
||||
);
|
||||
false
|
||||
// Standalone format can stay standalone only if there are no children.
|
||||
// If children were added in Studio, we must convert to directory format.
|
||||
if has_children {
|
||||
log::info!(
|
||||
"Syncback: Converting LocalScript {} from standalone to directory format (children added)",
|
||||
added.name
|
||||
);
|
||||
true
|
||||
} else {
|
||||
log::debug!(
|
||||
"Syncback: Preserving existing standalone format for LocalScript {}",
|
||||
added.name
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
ExistingFileFormat::None => has_children,
|
||||
};
|
||||
|
||||
if use_directory {
|
||||
// Only clean up if this is a new instance
|
||||
if existing_format == ExistingFileFormat::None {
|
||||
// Clean up standalone files if transitioning to directory (new instance or format conversion)
|
||||
if existing_format == ExistingFileFormat::None
|
||||
|| existing_format == ExistingFileFormat::Standalone
|
||||
{
|
||||
for suffix in &["local", "client"] {
|
||||
let standalone_path =
|
||||
parent_dir.join(format!("{}.{}.luau", added.name, suffix));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue