diff --git a/Cargo.lock b/Cargo.lock index 0cd6b9c..9b6c2b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 339a24f..dd7471a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/db/entities.rs b/src/db/entities.rs index 502062e..6e2e0ad 100644 --- a/src/db/entities.rs +++ b/src/db/entities.rs @@ -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"); + } + } +} diff --git a/src/item.rs b/src/item.rs index 4a447b3..2bc6135 100644 --- a/src/item.rs +++ b/src/item.rs @@ -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); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f639c59 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,5 @@ +pub mod db; +pub mod item; +pub mod music; +pub mod origins; +pub mod virtual_dirs; diff --git a/src/main.rs b/src/main.rs index aed6239..2484f3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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(); diff --git a/src/music/metadata.rs b/src/music/metadata.rs index 1329ff1..71f6f78 100644 --- a/src/music/metadata.rs +++ b/src/music/metadata.rs @@ -408,3 +408,124 @@ fn build_vorbis_comment(metadata: &MusicMetadata) -> Vec { 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); + } +} diff --git a/src/origins/local/file_io.rs b/src/origins/local/file_io.rs index e4792f2..6c8027b 100644 --- a/src/origins/local/file_io.rs +++ b/src/origins/local/file_io.rs @@ -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 = (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 = (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); + } +} diff --git a/src/origins/local/mod.rs b/src/origins/local/mod.rs index 6615fbe..e62940e 100644 --- a/src/origins/local/mod.rs +++ b/src/origins/local/mod.rs @@ -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); + } +} diff --git a/src/virtual_dirs.rs b/src/virtual_dirs.rs index a3a38ee..db5ce17 100644 --- a/src/virtual_dirs.rs +++ b/src/virtual_dirs.rs @@ -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"); + } + } +} diff --git a/tests/file_io_test.rs b/tests/file_io_test.rs new file mode 100644 index 0000000..52edb47 --- /dev/null +++ b/tests/file_io_test.rs @@ -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 = (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 = (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 = (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::>()); + assert_eq!(result, expected); +} diff --git a/tests/snapshot_test.rs b/tests/snapshot_test.rs new file mode 100644 index 0000000..8dcdd93 --- /dev/null +++ b/tests/snapshot_test.rs @@ -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"); +}