Add tests
This commit is contained in:
Generated
+49
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
pub mod db;
|
||||
pub mod item;
|
||||
pub mod music;
|
||||
pub mod origins;
|
||||
pub mod virtual_dirs;
|
||||
+2
-8
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user