use super::*;
#[test]
fn diff_empty_dir_against_populated_vfs_yields_no_results() {
let vfs_dir = TempDir::new("vfsdiff_base");
vfs_dir.write("textures/foo.dds", b"");
let vfs = VFS::from_directories(vec![vfs_dir.path()], None);
let empty = TempDir::new("vfsdiff_empty_mod");
let diff = vfs.diff_directory(empty.path());
assert!(diff.conflicts.is_empty());
assert!(diff.additions.is_empty());
}
#[test]
fn diff_dir_with_only_new_files_yields_only_additions() {
let vfs_dir = TempDir::new("vfsdiff_newfiles_base");
vfs_dir.write("textures/vanilla.dds", b"");
let vfs = VFS::from_directories(vec![vfs_dir.path()], None);
let mod_dir = TempDir::new("vfsdiff_newfiles_mod");
let new1 = mod_dir.write("meshes/new_mesh.nif", b"");
let new2 = mod_dir.write("textures/new_tex.dds", b"");
let diff = vfs.diff_directory(mod_dir.path());
assert!(diff.conflicts.is_empty());
assert_eq!(diff.additions.len(), 2);
let addition_paths: Vec<_> = diff
.additions
.iter()
.map(|(_, f)| f.path().to_path_buf())
.collect();
assert!(addition_paths.contains(&new1));
assert!(addition_paths.contains(&new2));
}
#[test]
fn diff_dir_with_only_conflicting_files_yields_only_conflicts() {
let vfs_dir = TempDir::new("vfsdiff_conflicts_base");
let existing = vfs_dir.write("textures/shared.dds", b"original");
let vfs = VFS::from_directories(vec![vfs_dir.path()], None);
let mod_dir = TempDir::new("vfsdiff_conflicts_mod");
let replacement = mod_dir.write("textures/shared.dds", b"replacement");
let diff = vfs.diff_directory(mod_dir.path());
assert!(diff.additions.is_empty());
assert_eq!(diff.conflicts.len(), 1);
let (key, incoming, displaced) = &diff.conflicts[0];
assert_eq!(key.as_os_str(), "textures/shared.dds");
assert_eq!(incoming.path(), replacement);
assert_eq!(displaced.path(), existing);
}
#[test]
fn diff_dir_mixed_yields_correct_split() {
let vfs_dir = TempDir::new("vfsdiff_mixed_base");
let existing = vfs_dir.write("textures/shared.dds", b"original");
vfs_dir.write("textures/untouched.dds", b"");
let vfs = VFS::from_directories(vec![vfs_dir.path()], None);
let mod_dir = TempDir::new("vfsdiff_mixed_mod");
let replacement = mod_dir.write("textures/shared.dds", b"mod_version");
let addition = mod_dir.write("meshes/new.nif", b"");
let diff = vfs.diff_directory(mod_dir.path());
assert_eq!(diff.conflicts.len(), 1);
assert_eq!(diff.additions.len(), 1);
let (_, incoming, displaced) = &diff.conflicts[0];
assert_eq!(incoming.path(), replacement);
assert_eq!(displaced.path(), existing);
assert_eq!(diff.additions[0].1.path(), addition);
}
#[test]
fn diff_dir_conflict_detected_across_case_normalization() {
let vfs_dir = TempDir::new("vfsdiff_case_base");
vfs_dir.write("textures/foo.dds", b"lowercase");
let vfs = VFS::from_directories(vec![vfs_dir.path()], None);
let mod_dir = TempDir::new("vfsdiff_case_mod");
let replacement = mod_dir.write("Textures/Foo.DDS", b"mod_version");
let diff = vfs.diff_directory(mod_dir.path());
assert_eq!(
diff.conflicts.len(),
1,
"case variant should be detected as conflict"
);
assert_eq!(diff.conflicts[0].1.path(), replacement);
}
#[test]
#[cfg(unix)]
fn diff_dir_skips_filenames_that_normalize_to_unsafe_keys() {
let vfs_dir = TempDir::new("vfsdiff_unsafe_base");
let vfs = VFS::from_directories(vec![vfs_dir.path()], None);
let mod_dir = TempDir::new("vfsdiff_unsafe_mod");
mod_dir.write("..\\escape.txt", b"escape");
let safe = mod_dir.write("safe.txt", b"safe");
let diff = vfs.diff_directory(mod_dir.path());
assert_eq!(diff.additions.len(), 1);
assert_eq!(diff.additions[0].0, PathBuf::from("safe.txt"));
assert_eq!(diff.additions[0].1.path(), safe);
assert!(diff.conflicts.is_empty());
}
#[test]
#[cfg(unix)]
fn diff_dir_uses_byte_keys_for_non_utf8_filenames() {
use std::ffi::OsString;
use std::io;
use std::os::unix::ffi::OsStringExt;
let file_name = OsString::from_vec(vec![b'f', 0xff, b'o', b'.', b'd', b'd', b's']);
let vfs_dir = TempDir::new("vfsdiff_non_utf8_base");
let base_file = vfs_dir.path().join(&file_name);
let write_result = fs::write(&base_file, b"base");
if let Err(err) = &write_result {
if err.raw_os_error() == Some(92) || err.kind() == io::ErrorKind::InvalidInput {
eprintln!("skipping non-UTF8 diff test: filesystem rejected byte filename: {err}");
return;
}
}
write_result.unwrap();
let vfs = VFS::from_directories(vec![vfs_dir.path()], None);
let mod_dir = TempDir::new("vfsdiff_non_utf8_mod");
let replacement = mod_dir.path().join(&file_name);
fs::write(&replacement, b"replacement").unwrap();
let diff = vfs.diff_directory(mod_dir.path());
assert_eq!(diff.conflicts.len(), 1);
assert!(diff.additions.is_empty());
assert_eq!(diff.conflicts[0].1.path(), replacement);
}
#[test]
fn diff_dir_handles_deep_nesting() {
let vfs_dir = TempDir::new("vfsdiff_deep_base");
vfs_dir.write("a/b/c/d/deep.txt", b"");
let vfs = VFS::from_directories(vec![vfs_dir.path()], None);
let mod_dir = TempDir::new("vfsdiff_deep_mod");
let replacement = mod_dir.write("a/b/c/d/deep.txt", b"mod");
let addition = mod_dir.write("a/b/c/d/new.txt", b"new");
let diff = vfs.diff_directory(mod_dir.path());
assert_eq!(diff.conflicts.len(), 1);
assert_eq!(diff.additions.len(), 1);
assert_eq!(diff.conflicts[0].1.path(), replacement);
assert_eq!(diff.additions[0].1.path(), addition);
}
#[test]
fn diff_dir_against_empty_vfs_yields_all_additions() {
let empty_base = TempDir::new("vfsdiff_emptyvfs_base");
let vfs = VFS::from_directories(vec![empty_base.path()], None);
let mod_dir = TempDir::new("vfsdiff_emptyvfs_mod");
mod_dir.write("a.txt", b"");
mod_dir.write("b.txt", b"");
let diff = vfs.diff_directory(mod_dir.path());
assert!(diff.conflicts.is_empty());
assert_eq!(diff.additions.len(), 2);
}