mirror of
https://github.com/rojo-rbx/rojo.git
synced 2026-10-23 05:51:39 +00:00
Update submodule and implement parallel I/O audit fixes
- Updated the `vscode-rojo` submodule to the latest commit. - Added a new plan for addressing issues found during the audit of parallelization and optimization commits, including fixes for a data loss bug in the orphan scanner, a duplicate-children bug in the prefetch cache, and a nested-detection correctness issue. - Implemented a small DRY refactor by extracting a shared helper function for UNC path stripping. - Enhanced the `syncback` logic to ensure hidden directories are preserved during clean mode operations and added tests to validate these changes. These updates aim to improve the reliability and correctness of the parallel I/O functionality while maintaining code clarity and performance.
This commit is contained in:
parent
1c25b928a8
commit
43834f357e
6 changed files with 536 additions and 57 deletions
|
|
@ -302,3 +302,167 @@ fn parallel_snapshot_error_propagation() {
|
|||
"Should propagate JSON parse error from parallel snapshot"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlapping_path_roots_no_duplicate_children() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let dir = tempdir().expect("couldn't create temp dir");
|
||||
let root = dir.path();
|
||||
let src = root.join("src");
|
||||
let shared = src.join("shared");
|
||||
fs::create_dir_all(&shared).unwrap();
|
||||
|
||||
fs::write(src.join("top.luau"), "return 1").unwrap();
|
||||
fs::write(shared.join("a.luau"), "return 2").unwrap();
|
||||
fs::write(shared.join("b.luau"), "return 3").unwrap();
|
||||
|
||||
fs::write(
|
||||
root.join("default.project.json5"),
|
||||
r#"{
|
||||
"name": "OverlapTest",
|
||||
"tree": {
|
||||
"$path": "src",
|
||||
"shared": {
|
||||
"$path": "src/shared"
|
||||
}
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let ctx = librojo::InstanceContext::default();
|
||||
let project_path = root.join("default.project.json5");
|
||||
|
||||
use std::collections::HashMap;
|
||||
let mut files = HashMap::new();
|
||||
let mut canonical = HashMap::new();
|
||||
let mut is_file = HashMap::new();
|
||||
let mut children_map: HashMap<std::path::PathBuf, Vec<std::path::PathBuf>> = HashMap::new();
|
||||
|
||||
for entry in walkdir::WalkDir::new(root).follow_links(true) {
|
||||
let entry = entry.unwrap();
|
||||
let path = entry.path().to_path_buf();
|
||||
is_file.insert(path.clone(), entry.file_type().is_file());
|
||||
if entry.file_type().is_file() {
|
||||
files.insert(path.clone(), fs::read(&path).unwrap());
|
||||
}
|
||||
if let Ok(c) = fs::canonicalize(&path) {
|
||||
canonical.insert(path.clone(), c);
|
||||
}
|
||||
if entry.depth() > 0 {
|
||||
if let Some(parent) = entry.path().parent() {
|
||||
children_map
|
||||
.entry(parent.to_path_buf())
|
||||
.or_default()
|
||||
.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for children in children_map.values_mut() {
|
||||
children.sort_by(|a, b| a.file_name().cmp(&b.file_name()));
|
||||
}
|
||||
|
||||
let vfs = memofs::Vfs::new_default();
|
||||
vfs.set_prefetch_cache(memofs::PrefetchCache {
|
||||
files,
|
||||
canonical,
|
||||
is_file,
|
||||
children: children_map,
|
||||
});
|
||||
|
||||
let snap = librojo::snapshot_from_vfs(&ctx, &vfs, &project_path)
|
||||
.expect("snapshot failed")
|
||||
.expect("snapshot returned None");
|
||||
|
||||
fn find_child<'a>(
|
||||
snap: &'a librojo::InstanceSnapshot,
|
||||
name: &str,
|
||||
) -> Option<&'a librojo::InstanceSnapshot> {
|
||||
snap.children.iter().find(|c| c.name.as_ref() == name)
|
||||
}
|
||||
|
||||
let shared_snap = find_child(&snap, "shared").expect("shared child should exist");
|
||||
let shared_names: Vec<&str> = shared_snap
|
||||
.children
|
||||
.iter()
|
||||
.map(|c| c.name.as_ref())
|
||||
.collect();
|
||||
|
||||
let mut deduped = shared_names.clone();
|
||||
deduped.sort();
|
||||
deduped.dedup();
|
||||
assert_eq!(
|
||||
shared_names.len(),
|
||||
deduped.len(),
|
||||
"No duplicate children should exist under shared. Got: {:?}",
|
||||
shared_names
|
||||
);
|
||||
}
|
||||
|
||||
/// Test the nested path detection algorithm used in clean-mode orphan removal.
|
||||
/// Sorted path walk with ancestor tracking should only emit top-level paths.
|
||||
#[test]
|
||||
fn nested_path_detection_correctness() {
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn top_level_paths(input: &[&str]) -> Vec<PathBuf> {
|
||||
let mut sorted: Vec<PathBuf> = input.iter().map(PathBuf::from).collect();
|
||||
sorted.sort();
|
||||
|
||||
let mut result = Vec::new();
|
||||
let mut current_ancestor: Option<&PathBuf> = None;
|
||||
for path in &sorted {
|
||||
if let Some(ancestor) = current_ancestor {
|
||||
if path.starts_with(ancestor) && path != ancestor {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
current_ancestor = Some(path);
|
||||
result.push(path.clone());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
let result = top_level_paths(&["a/b", "a/b/c", "a/b/d", "x/y", "x/y/z"]);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![PathBuf::from("a/b"), PathBuf::from("x/y")],
|
||||
"Should only keep top-level ancestors"
|
||||
);
|
||||
|
||||
let result = top_level_paths(&["a/b/c", "a/b/d", "a/b"]);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![PathBuf::from("a/b")],
|
||||
"Should detect ancestors regardless of input order"
|
||||
);
|
||||
|
||||
let result = top_level_paths(&["a", "a/b", "a/b/c"]);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![PathBuf::from("a")],
|
||||
"Single ancestor should subsume all descendants"
|
||||
);
|
||||
|
||||
let result = top_level_paths(&["a/b", "a/c", "b/d"]);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![
|
||||
PathBuf::from("a/b"),
|
||||
PathBuf::from("a/c"),
|
||||
PathBuf::from("b/d")
|
||||
],
|
||||
"Non-nested siblings should all be kept"
|
||||
);
|
||||
|
||||
let result = top_level_paths(&["x"]);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![PathBuf::from("x")],
|
||||
"Single path should be kept"
|
||||
);
|
||||
|
||||
let result: Vec<PathBuf> = top_level_paths(&[]);
|
||||
assert!(result.is_empty(), "Empty input should produce empty output");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -328,3 +328,56 @@ fn clean_mode_is_idempotent() {
|
|||
"File content should be identical after second syncback"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test that clean mode does NOT delete hidden directories or their contents
|
||||
/// inside $path source directories. Hidden directories like .vscode/ or
|
||||
/// .cursor/ should be left untouched by the orphan scanner.
|
||||
#[test]
|
||||
fn clean_mode_preserves_hidden_directories() {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let source_path = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("sync_rules")
|
||||
.join("input-project");
|
||||
let input_file = Path::new(SYNCBACK_TESTS_PATH)
|
||||
.join("sync_rules")
|
||||
.join("input.rbxm");
|
||||
|
||||
let test_dir = tempdir().expect("Couldn't create temporary directory");
|
||||
let project_path = test_dir.path().join("test_project");
|
||||
|
||||
fs_err::create_dir(&project_path).expect("Couldn't create project directory");
|
||||
copy_recursive(&source_path, &project_path).expect("Couldn't copy project");
|
||||
|
||||
let src_dir = project_path.join("src");
|
||||
let hidden_dir = src_dir.join(".hidden");
|
||||
fs_err::create_dir_all(&hidden_dir).expect("Couldn't create hidden directory");
|
||||
fs_err::write(hidden_dir.join("keep.txt"), "do not delete").unwrap();
|
||||
fs_err::write(hidden_dir.join("config.txt"), "some config").unwrap();
|
||||
|
||||
let nested_hidden = hidden_dir.join("nested");
|
||||
fs_err::create_dir_all(&nested_hidden).unwrap();
|
||||
fs_err::write(nested_hidden.join("deep.txt"), "also keep").unwrap();
|
||||
|
||||
assert!(
|
||||
run_syncback(&project_path, &input_file, false),
|
||||
"Syncback should succeed"
|
||||
);
|
||||
|
||||
assert!(
|
||||
hidden_dir.exists(),
|
||||
".hidden directory should survive clean mode syncback"
|
||||
);
|
||||
assert!(
|
||||
hidden_dir.join("keep.txt").exists(),
|
||||
".hidden/keep.txt should survive"
|
||||
);
|
||||
assert!(
|
||||
hidden_dir.join("config.txt").exists(),
|
||||
".hidden/config.txt should survive"
|
||||
);
|
||||
assert!(
|
||||
nested_hidden.join("deep.txt").exists(),
|
||||
".hidden/nested/deep.txt should survive"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue