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