Add tests

This commit is contained in:
Alexander
2026-06-27 20:11:33 +02:00
parent 53e6dbaef0
commit 24fd81ff9e
12 changed files with 837 additions and 8 deletions
Generated
+49
View File
@@ -828,6 +828,16 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "etcetera"
version = "0.11.0"
@@ -855,6 +865,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
[[package]]
name = "fastrand"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
@@ -1436,6 +1452,12 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
[[package]]
name = "litemap"
version = "0.8.2"
@@ -1516,6 +1538,7 @@ dependencies = [
"notify",
"sea-orm",
"symphonia",
"tempfile",
"time",
"tokio",
"twox-hash",
@@ -2110,6 +2133,19 @@ dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.13.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
]
[[package]]
name = "rustversion"
version = "1.0.22"
@@ -2803,6 +2839,19 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
[[package]]
name = "tempfile"
version = "3.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "2.0.18"
+3
View File
@@ -17,3 +17,6 @@ twox-hash = "2.1.2"
sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "runtime-tokio", "macros" ] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
[dev-dependencies]
tempfile = "3"
+95
View File
@@ -60,3 +60,98 @@ impl From<(Model, fs::Metadata)> for Item {
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::item::{FileType, Item};
#[test]
fn item_to_active_model_fields_match() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(42),
INodeNo::ROOT,
"test".to_string(),
PathBuf::from("/original/path"),
PathBuf::from("local/path"),
FileType::Directory,
metadata,
None,
);
let am = ActiveModel::from(&item);
if let sea_orm::ActiveValue::Set(v) = &am.inode {
assert_eq!(*v, 42i64);
} else {
panic!("inode field not set");
}
if let sea_orm::ActiveValue::Set(v) = &am.name {
assert_eq!(v, "test");
} else {
panic!("name field not set");
}
if let sea_orm::ActiveValue::Set(v) = &am.original_path {
assert_eq!(v, "/original/path");
} else {
panic!("original_path field not set");
}
if let sea_orm::ActiveValue::Set(v) = &am.local_path {
assert_eq!(v, "local/path");
} else {
panic!("local_path field not set");
}
if let sea_orm::ActiveValue::Set(v) = &am.hash {
assert_eq!(*v, item.hash as i64);
} else {
panic!("hash field not set");
}
}
#[test]
fn file_type_string_mapping() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item_dir = Item::new(
INodeNo(1),
INodeNo::ROOT,
"dir".to_string(),
PathBuf::from("/original/dir"),
PathBuf::from("local/dir"),
FileType::Directory,
metadata.clone(),
None,
);
let am_dir = ActiveModel::from(&item_dir);
if let sea_orm::ActiveValue::Set(v) = &am_dir.file_type {
assert_eq!(v, "directory");
} else {
panic!("file_type field not set for directory");
}
let item_file = Item::new(
INodeNo(2),
INodeNo::ROOT,
"file".to_string(),
PathBuf::from("/original/file"),
PathBuf::from("local/file"),
FileType::File,
metadata,
None,
);
let am_file = ActiveModel::from(&item_file);
if let sea_orm::ActiveValue::Set(v) = &am_file.file_type {
assert_eq!(v, "file");
} else {
panic!("file_type field not set for file");
}
}
}
+67
View File
@@ -82,3 +82,70 @@ impl Item {
return hasher.finish();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_hash_deterministic() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item1 = Item::new(
INodeNo(42),
INodeNo::ROOT,
"test".to_string(),
PathBuf::from("/some/path"),
PathBuf::from("test"),
FileType::File,
metadata,
None,
);
let metadata2 = std::fs::metadata(tmp.path()).unwrap();
let item2 = Item::new(
INodeNo(42),
INodeNo::ROOT,
"test".to_string(),
PathBuf::from("/some/path"),
PathBuf::from("test"),
FileType::File,
metadata2,
None,
);
assert_eq!(item1.hash, item2.hash);
}
#[test]
fn compute_hash_changes_on_path_change() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item1 = Item::new(
INodeNo(42),
INodeNo::ROOT,
"test".to_string(),
PathBuf::from("/some/path"),
PathBuf::from("test"),
FileType::File,
metadata,
None,
);
let metadata2 = std::fs::metadata(tmp.path()).unwrap();
let item2 = Item::new(
INodeNo(42),
INodeNo::ROOT,
"test".to_string(),
PathBuf::from("/different/path"),
PathBuf::from("test"),
FileType::File,
metadata2,
None,
);
assert_ne!(item1.hash, item2.hash);
}
}
+5
View File
@@ -0,0 +1,5 @@
pub mod db;
pub mod item;
pub mod music;
pub mod origins;
pub mod virtual_dirs;
+2 -8
View File
@@ -1,15 +1,9 @@
use clap::Parser;
use origins::local::watcher::FileWatcher;
use musicfs::origins::local::watcher::FileWatcher;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
mod db;
mod item;
mod music;
mod origins;
mod virtual_dirs;
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
@@ -39,7 +33,7 @@ async fn main() {
let db = sea_orm::Database::connect(&args.database).await.unwrap();
let fs = origins::local::LocalOrigin::new(args.source.clone(), mountpoint.clone(), db)
let fs = musicfs::origins::local::LocalOrigin::new(args.source.clone(), mountpoint.clone(), db)
.await
.unwrap();
+121
View File
@@ -408,3 +408,124 @@ fn build_vorbis_comment(metadata: &MusicMetadata) -> Vec<u8> {
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn read_block_header_normal() {
let hdr = [0x04, 0x00, 0x01, 0x00];
let (is_last, block_type, length) = read_block_header(&hdr);
assert!(!is_last);
assert_eq!(block_type, 4);
assert_eq!(length, 256);
}
#[test]
fn read_block_header_last() {
let hdr = [0x84, 0x00, 0x00, 0x10];
let (is_last, block_type, length) = read_block_header(&hdr);
assert!(is_last);
assert_eq!(block_type, 4);
assert_eq!(length, 16);
}
#[test]
fn virtual_size_calculation() {
let mut metadata = MusicMetadata::default();
metadata.header = vec![0u8; 100];
metadata.picture_data_ranges = vec![(0, 50)];
metadata.real_audio_start = 200;
let file_size = 1000u64;
let virtual_size = metadata.virtual_size(file_size);
assert_eq!(virtual_size, 954);
}
#[test]
fn build_vorbis_comment_roundtrip() {
let mut metadata = MusicMetadata::default();
metadata.artist = vec!["Artist1".to_string(), "Artist2".to_string()];
metadata.album = "Album".to_string();
metadata.track_number = 3;
metadata.track_title = "Title".to_string();
metadata.other_tags = vec!["GENRE=Rock".to_string()];
let vorbis_bytes = build_vorbis_comment(&metadata);
let mut parsed = MusicMetadata::default();
parse_vorbis_comment_block(&vorbis_bytes, &mut parsed);
assert_eq!(parsed.artist, vec!["Artist1", "Artist2"]);
assert_eq!(parsed.album, "Album");
assert_eq!(parsed.track_number, 3);
assert_eq!(parsed.track_title, "Title");
assert_eq!(parsed.other_tags, vec!["GENRE=Rock"]);
}
#[test]
fn parse_vorbis_comment_multi_artist() {
let mut vorbis_bytes = Vec::new();
let vendor = b"test";
vorbis_bytes.extend_from_slice(&(vendor.len() as u32).to_le_bytes());
vorbis_bytes.extend_from_slice(vendor);
let comments = vec!["ARTIST=Artist1", "ARTIST=Artist2"];
vorbis_bytes.extend_from_slice(&(comments.len() as u32).to_le_bytes());
for comment in &comments {
let bytes = comment.as_bytes();
vorbis_bytes.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
vorbis_bytes.extend_from_slice(bytes);
}
let mut metadata = MusicMetadata::default();
parse_vorbis_comment_block(&vorbis_bytes, &mut metadata);
assert_eq!(metadata.artist.len(), 2);
assert_eq!(metadata.artist[0], "Artist1");
assert_eq!(metadata.artist[1], "Artist2");
}
#[test]
fn extract_non_vorbis_blocks_strips_vc_and_padding() {
let mut header = Vec::new();
header.extend_from_slice(b"fLaC");
let streaminfo_data = vec![0u8; 34];
header.push(0x00);
header.extend_from_slice(&[0x00, 0x00, 0x22]);
header.extend_from_slice(&streaminfo_data);
let vorbis_data = vec![0u8; 50];
header.push(0x04);
header.extend_from_slice(&[0x00, 0x00, 0x32]);
header.extend_from_slice(&vorbis_data);
let padding_data = vec![0u8; 100];
header.push(0x81);
header.extend_from_slice(&[0x00, 0x00, 0x64]);
header.extend_from_slice(&padding_data);
let blocks = extract_non_vorbis_blocks(&header);
assert_eq!(blocks.len(), 1);
assert_eq!(blocks[0].0, 0);
assert_eq!(blocks[0].1.len(), 34);
}
#[test]
fn find_vorbis_offsets_correct() {
let mut metadata = MusicMetadata::default();
metadata.artist = vec!["TestArtist".to_string()];
metadata.album = "TestAlbum".to_string();
metadata.track_number = 1;
metadata.track_title = "TestTitle".to_string();
let other_blocks = vec![(0, vec![0u8; 34])];
let (header, expected_offset, expected_length) = build_flac_header(other_blocks, &metadata);
metadata.header = header;
metadata.find_vorbis_offsets();
assert_eq!(metadata.vorbis_comment_offset, expected_offset);
assert_eq!(metadata.vorbis_comment_length, expected_length);
}
}
+68
View File
@@ -75,3 +75,71 @@ pub fn assemble_flac_read(
return Ok(buf);
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
#[test]
fn read_bytes_at_full_file() {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(b"hello world").unwrap();
f.flush().unwrap();
let path = f.path();
let result = read_bytes_at(path, 0, 11).unwrap();
assert_eq!(result, b"hello world");
}
#[test]
fn read_bytes_at_offset() {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(b"abcdefghij").unwrap();
f.flush().unwrap();
let path = f.path();
let result = read_bytes_at(path, 3, 4).unwrap();
assert_eq!(result, b"defg");
}
#[test]
fn assemble_flac_read_header_only() {
let header = b"HEADERDATA";
let result = assemble_flac_read(Path::new(""), header, &[], &[], 0, 2, 4).unwrap();
assert_eq!(result, b"ADER");
}
#[test]
fn assemble_flac_read_audio_region() {
let mut f = tempfile::NamedTempFile::new().unwrap();
let content: Vec<u8> = (0..20).collect();
f.write_all(&content).unwrap();
f.flush().unwrap();
let path = f.path();
let header = b"HDR";
let result = assemble_flac_read(path, header, &[], &[], 10, 5, 4).unwrap();
// offset=5, header_len=3, so we read from header[3..] (0 bytes) + audio at real_off=10+(5-3)=12
// content[12..16] = [12, 13, 14, 15]
assert_eq!(result, vec![12, 13, 14, 15]);
}
#[test]
fn assemble_flac_read_header_audio_boundary() {
let mut f = tempfile::NamedTempFile::new().unwrap();
let content: Vec<u8> = (0..110).collect();
f.write_all(&content).unwrap();
f.flush().unwrap();
let path = f.path();
let header = b"ABCD";
let result = assemble_flac_read(path, header, &[], &[], 100, 2, 6).unwrap();
// offset=2, size=6, header_len=4
// First 2 bytes from header[2..4] = "CD"
// Remaining 4 bytes from audio at real_off=100+(2+2-4)=100
// content[100..104] = [100, 101, 102, 103]
let expected = [b'C', b'D', 100, 101, 102, 103];
assert_eq!(result, expected);
}
}
+94
View File
@@ -466,3 +466,97 @@ impl Filesystem for LocalOrigin {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::item::Item;
use crate::music::metadata::MusicMetadata;
use std::os::unix::fs::MetadataExt;
use std::path::PathBuf;
#[test]
fn file_to_attr_uses_real_size_for_non_flac() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(42),
INodeNo::ROOT,
"test".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test"),
FileType::File,
metadata.clone(),
None,
);
let attr = LocalOrigin::file_to_attr(&item);
assert_eq!(attr.size, metadata.size());
}
#[test]
fn file_to_attr_uses_virtual_size_for_flac() {
let tmp = tempfile::tempdir().unwrap();
let test_file = tmp.path().join("test.flac");
std::fs::write(&test_file, vec![0u8; 1000]).unwrap();
let metadata = std::fs::metadata(&test_file).unwrap();
let mm = MusicMetadata {
real_audio_start: 500,
header: vec![0u8; 100],
picture_data_ranges: vec![(0, 50)],
..MusicMetadata::default()
};
let item = Item::new(
INodeNo(43),
INodeNo::ROOT,
"test_flac".to_string(),
test_file.clone(),
PathBuf::from("test_flac"),
FileType::File,
metadata.clone(),
Some(mm.clone()),
);
let attr = LocalOrigin::file_to_attr(&item);
let expected_virtual_size = mm.virtual_size(metadata.size());
assert_eq!(attr.size, expected_virtual_size);
}
#[test]
fn file_to_attr_kind_directory() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(44),
INodeNo::ROOT,
"test_dir".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test_dir"),
FileType::Directory,
metadata,
None,
);
let attr = LocalOrigin::file_to_attr(&item);
assert_eq!(attr.kind, fuser::FileType::Directory);
}
#[test]
fn file_to_attr_kind_file() {
let tmp = tempfile::tempdir().unwrap();
let metadata = std::fs::metadata(tmp.path()).unwrap();
let item = Item::new(
INodeNo(45),
INodeNo::ROOT,
"test_file".to_string(),
tmp.path().to_path_buf(),
PathBuf::from("test_file"),
FileType::File,
metadata,
None,
);
let attr = LocalOrigin::file_to_attr(&item);
assert_eq!(attr.kind, fuser::FileType::RegularFile);
}
}
+145
View File
@@ -119,3 +119,148 @@ pub fn restore_virtual_paths(
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn virtual_inode_deterministic() {
let ino1 = virtual_inode("foo");
let ino2 = virtual_inode("foo");
assert_eq!(ino1, ino2);
}
#[test]
fn virtual_inode_high_bit_set() {
let ino = virtual_inode("test");
assert_eq!(ino.0 & (1u64 << 63), 1u64 << 63);
}
#[test]
fn virtual_inode_different_inputs() {
let ino1 = virtual_inode("foo");
let ino2 = virtual_inode("bar");
assert_ne!(ino1, ino2);
}
#[test]
fn parent_inode_single_component() {
let ino = parent_inode_from_path(Path::new("file.txt"));
assert_eq!(ino, INodeNo::ROOT);
}
#[test]
fn parent_inode_multi_component() {
let ino = parent_inode_from_path(Path::new("a/b/c"));
let expected = virtual_inode("a/b");
assert_eq!(ino, expected);
}
#[test]
fn parent_inode_absolute_path_filtered() {
let ino = parent_inode_from_path(Path::new("/file"));
assert_eq!(ino, INodeNo::ROOT);
}
#[test]
fn ensure_virtual_dirs_single_component() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
let ino = ensure_virtual_dirs(Path::new("file.txt"), source, &mut map);
assert_eq!(ino, INodeNo::ROOT);
assert!(map.is_empty());
}
#[test]
fn ensure_virtual_dirs_creates_hierarchy() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
let ino = ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let a_ino = virtual_inode("a");
let ab_ino = virtual_inode("a/b");
assert_eq!(ino, ab_ino);
assert_eq!(map.len(), 2);
assert!(map.contains_key(&a_ino));
assert!(map.contains_key(&ab_ino));
let a_item = &map[&a_ino];
assert_eq!(a_item.parent_inode, INodeNo::ROOT);
let ab_item = &map[&ab_ino];
assert_eq!(ab_item.parent_inode, a_ino);
}
#[test]
fn ensure_virtual_dirs_idempotent() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut map = BTreeMap::new();
ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let size_after_first = map.len();
ensure_virtual_dirs(Path::new("a/b/file"), source, &mut map);
let size_after_second = map.len();
assert_eq!(size_after_first, size_after_second);
}
#[test]
fn restore_virtual_paths_skips_root() {
let tmp = tempfile::tempdir().unwrap();
let source = tmp.path();
let mut snapshot = BTreeMap::new();
let real_ino = INodeNo(42);
let real_item = Item::new(
real_ino,
INodeNo::ROOT,
"real_file".to_string(),
source.to_path_buf(),
PathBuf::from("real_file"),
FileType::File,
fs::metadata(source).unwrap(),
None,
);
snapshot.insert(real_ino, real_item);
let mut db_items = HashMap::new();
db_items.insert(
1i64,
Model {
inode: 1,
hash: 0,
name: "root".to_string(),
original_path: "/tmp/test".to_string(),
local_path: "/tmp/test".to_string(),
file_type: "directory".to_string(),
},
);
db_items.insert(
42i64,
Model {
inode: 42,
hash: 0,
name: "real_file".to_string(),
original_path: "real_file".to_string(),
local_path: "real_file".to_string(),
file_type: "file".to_string(),
},
);
restore_virtual_paths(&mut snapshot, &db_items, source);
for item in snapshot.values() {
assert_ne!(item.name, "/");
assert_ne!(item.name, "tmp");
}
}
}
+75
View File
@@ -0,0 +1,75 @@
use std::io::Write;
use musicfs::origins::local::file_io::assemble_flac_read;
#[test]
fn assemble_flac_read_picture_header_region() {
let mut f = tempfile::NamedTempFile::new().unwrap();
let data: Vec<u8> = (0..100).collect();
f.write_all(&data).unwrap();
f.flush().unwrap();
let path = f.path();
let header = b"ABCD";
let pic_hdr = [0x86, 0x00, 0x00, 0x05];
let pic_ranges = [(10u64, 5u64)];
let real_audio_start = 50u64;
let result = assemble_flac_read(
path,
header,
&[pic_hdr],
&pic_ranges,
real_audio_start,
4,
4,
)
.unwrap();
assert_eq!(result, pic_hdr);
}
#[test]
fn assemble_flac_read_picture_data_region() {
let mut f = tempfile::NamedTempFile::new().unwrap();
let data: Vec<u8> = (0..100).collect();
f.write_all(&data).unwrap();
f.flush().unwrap();
let path = f.path();
let header = b"ABCD";
let pic_hdr = [0x86, 0x00, 0x00, 0x05];
let pic_ranges = [(10u64, 5u64)];
let real_audio_start = 50u64;
let result = assemble_flac_read(
path,
header,
&[pic_hdr],
&pic_ranges,
real_audio_start,
8,
5,
)
.unwrap();
assert_eq!(result, vec![10, 11, 12, 13, 14]);
}
#[test]
fn assemble_flac_read_full_virtual_layout() {
let mut f = tempfile::NamedTempFile::new().unwrap();
let data: Vec<u8> = (0..200).map(|i| i as u8).collect();
f.write_all(&data).unwrap();
f.flush().unwrap();
let path = f.path();
let header = vec![0xAA; 8];
let real_audio_start = 100u64;
let result = assemble_flac_read(path, &header, &[], &[], real_audio_start, 0, 18).unwrap();
let mut expected = vec![0xAA; 8];
expected.extend_from_slice(&(100..110).map(|i| i as u8).collect::<Vec<u8>>());
assert_eq!(result, expected);
}
+113
View File
@@ -0,0 +1,113 @@
use std::fs;
use std::io::Write;
use std::sync::{Arc, Mutex};
use fuser::INodeNo;
use musicfs::item::FileType;
use musicfs::origins::local::snapshot::{build_snapshot, fill_fileset};
#[test]
fn build_snapshot_empty_dir() {
let source = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
let snapshot = build_snapshot(source.path(), dest.path()).unwrap();
assert_eq!(snapshot.len(), 1);
assert!(snapshot.contains_key(&INodeNo::ROOT));
let root = &snapshot[&INodeNo::ROOT];
assert_eq!(root.inode, INodeNo::ROOT);
assert_eq!(root.parent_inode, INodeNo::ROOT);
assert_eq!(root.file_type, FileType::Directory);
}
#[test]
fn build_snapshot_flat_files() {
let source = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
let mut f = fs::File::create(source.path().join("a.txt")).unwrap();
f.write_all(b"content a").unwrap();
let mut f = fs::File::create(source.path().join("b.txt")).unwrap();
f.write_all(b"content b").unwrap();
let mut f = fs::File::create(source.path().join("c.txt")).unwrap();
f.write_all(b"content c").unwrap();
let snapshot = build_snapshot(source.path(), dest.path()).unwrap();
assert_eq!(snapshot.len(), 4);
assert!(snapshot.contains_key(&INodeNo::ROOT));
let file_entries: Vec<_> = snapshot
.values()
.filter(|item| item.file_type == FileType::File)
.collect();
assert_eq!(file_entries.len(), 3);
for file_entry in file_entries {
assert_eq!(file_entry.parent_inode, INodeNo::ROOT);
}
}
#[test]
fn build_snapshot_nested_dirs() {
let source = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
fs::create_dir(source.path().join("subdir")).unwrap();
let mut f = fs::File::create(source.path().join("subdir").join("file.txt")).unwrap();
f.write_all(b"nested content").unwrap();
let snapshot = build_snapshot(source.path(), dest.path()).unwrap();
assert!(snapshot.contains_key(&INodeNo::ROOT));
let subdir_entry = snapshot
.values()
.find(|item| item.name == "subdir" && item.file_type == FileType::Directory)
.expect("subdir not found");
let file_entry = snapshot
.values()
.find(|item| item.name == "file.txt" && item.file_type == FileType::File)
.expect("file.txt not found");
assert_eq!(subdir_entry.parent_inode, INodeNo::ROOT);
assert_eq!(file_entry.parent_inode, INodeNo::ROOT);
}
#[test]
fn fill_fileset_removes_deleted() {
let source = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
let mut f = fs::File::create(source.path().join("file1.txt")).unwrap();
f.write_all(b"content 1").unwrap();
let mut f = fs::File::create(source.path().join("file2.txt")).unwrap();
f.write_all(b"content 2").unwrap();
let initial_snapshot = build_snapshot(source.path(), dest.path()).unwrap();
let map = Arc::new(Mutex::new(initial_snapshot));
assert_eq!(map.lock().unwrap().len(), 3);
fs::remove_file(source.path().join("file1.txt")).unwrap();
fill_fileset(&map, source.path(), dest.path());
let final_map = map.lock().unwrap();
assert_eq!(final_map.len(), 2);
assert!(final_map.contains_key(&INodeNo::ROOT));
let remaining_files: Vec<_> = final_map
.values()
.filter(|item| item.file_type == FileType::File)
.collect();
assert_eq!(remaining_files.len(), 1);
assert_eq!(remaining_files[0].name, "file2.txt");
}