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:
Joe 2026-02-19 04:36:44 -06:00
parent 1c25b928a8
commit 43834f357e
6 changed files with 536 additions and 57 deletions

View file

@ -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");
}

View file

@ -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"
);
}