Files
MusicFS/musicfs/crates/musicfs-cas/tests/integration.rs
T
Alexander 32c96701c8 Implement Week 5 CDC & Delta Detection with Oracle fixes
- Add CdcChunker using FastCDC v3 (16KB/64KB/256KB chunks)
- Add DeltaDetector with scan_origin() returning ScannedFile (no FileId assignment)
- Add OriginWatcher with inotify and 200ms debounce using tokio::spawn
- Fix LocalOrigin::read() to loop until all bytes read
- Add read_full() method to Origin trait
- Add mtime field to ChunkManifest
- Update ContentFetcher to use CDC chunking
- Update bandwidth reduction test to assert >90% (NFR-6.4)

Tests: 71 pass (+11 new)
2026-05-12 20:05:44 +02:00

198 lines
5.9 KiB
Rust

use musicfs_cache::TreeBuilder;
use musicfs_cas::{CasConfig, CasStore, ChunkManifest, ChunkRef, ContentFetcher, FileReader};
use musicfs_core::{FileId, FileMeta, OriginId, RealPath, VirtualPath};
use musicfs_origins::LocalOrigin;
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use std::time::SystemTime;
use tempfile::TempDir;
fn make_file_meta(id: i64, vpath: &str, size: u64) -> FileMeta {
FileMeta {
id: FileId(id),
virtual_path: VirtualPath::new(vpath),
real_path: RealPath {
origin_id: OriginId::from("test"),
path: PathBuf::from("/test"),
},
size,
mtime: SystemTime::now(),
content_hash: None,
audio: None,
}
}
#[tokio::test]
async fn test_cas_and_tree_integration() {
let dir = TempDir::new().unwrap();
let config = CasConfig {
chunks_dir: dir.path().join("chunks"),
..Default::default()
};
let store = Arc::new(CasStore::open(config).await.unwrap());
let file_data = b"This is test audio file content for testing.";
let chunk_hash = store.put(file_data).await.unwrap();
let mut builder = TreeBuilder::new();
builder.add_file(&make_file_meta(
1,
"/Artist/Album/Track.flac",
file_data.len() as u64,
));
let _tree = Arc::new(RwLock::new(builder.build()));
let reader = Arc::new(FileReader::new(store.clone()));
reader.register_manifest(ChunkManifest {
file_id: FileId(1),
total_size: file_data.len() as u64,
mtime: 0,
chunks: vec![ChunkRef {
hash: chunk_hash,
offset: 0,
size: file_data.len() as u32,
}],
});
let result = reader
.read(FileId(1), 0, file_data.len() as u32)
.await
.unwrap();
assert_eq!(&result[..], file_data);
}
#[tokio::test]
async fn test_cache_persistence() {
let dir = TempDir::new().unwrap();
let config = CasConfig {
chunks_dir: dir.path().join("chunks"),
..Default::default()
};
let data = b"persistent data";
let hash = {
let store = CasStore::open(config.clone()).await.unwrap();
store.put(data).await.unwrap()
};
let store = CasStore::open(config).await.unwrap();
let retrieved = store.get(&hash).await.unwrap();
assert_eq!(&retrieved[..], data);
}
#[tokio::test]
async fn test_deduplication() {
let dir = TempDir::new().unwrap();
let config = CasConfig {
chunks_dir: dir.path().join("chunks"),
..Default::default()
};
let store = CasStore::open(config).await.unwrap();
let data = b"duplicate this content";
let hash1 = store.put(data).await.unwrap();
let size_after_first = store.current_size();
let hash2 = store.put(data).await.unwrap();
let size_after_second = store.current_size();
assert_eq!(hash1, hash2);
assert_eq!(size_after_first, size_after_second);
}
#[tokio::test]
async fn test_fetcher_cache_miss_flow() {
let origin_dir = TempDir::new().unwrap();
let cas_dir = TempDir::new().unwrap();
let test_content = b"This is audio content that will be fetched on cache miss";
let test_file_path = origin_dir.path().join("test.flac");
std::fs::write(&test_file_path, test_content).unwrap();
let config = CasConfig {
chunks_dir: cas_dir.path().join("chunks"),
..Default::default()
};
let store = Arc::new(CasStore::open(config).await.unwrap());
let origin_id = OriginId::from("test-origin");
let origin = Arc::new(LocalOrigin::new(origin_id.clone(), origin_dir.path().to_path_buf()));
let fetcher = ContentFetcher::new(store.clone());
fetcher.register_origin(origin);
let file_id = FileId(42);
let file_meta = FileMeta {
id: file_id,
virtual_path: VirtualPath::new("/Artist/Album/test.flac"),
real_path: RealPath {
origin_id,
path: PathBuf::from("/test.flac"),
},
size: test_content.len() as u64,
mtime: SystemTime::now(),
content_hash: None,
audio: None,
};
fetcher.register_file(file_meta);
let manifest = fetcher.fetch_file(file_id).await.unwrap();
assert_eq!(manifest.file_id, file_id);
assert_eq!(manifest.total_size, test_content.len() as u64);
assert_eq!(manifest.chunks.len(), 1);
let chunk_data = store.get(&manifest.chunks[0].hash).await.unwrap();
assert_eq!(&chunk_data[..], test_content);
}
#[tokio::test]
async fn test_reader_with_fetcher_integration() {
let origin_dir = TempDir::new().unwrap();
let cas_dir = TempDir::new().unwrap();
let test_content = b"Audio file content for reader integration test";
let test_file_path = origin_dir.path().join("song.flac");
std::fs::write(&test_file_path, test_content).unwrap();
let config = CasConfig {
chunks_dir: cas_dir.path().join("chunks"),
..Default::default()
};
let store = Arc::new(CasStore::open(config).await.unwrap());
let origin_id = OriginId::from("local");
let origin = Arc::new(LocalOrigin::new(origin_id.clone(), origin_dir.path().to_path_buf()));
let fetcher = ContentFetcher::new(store.clone());
fetcher.register_origin(origin);
let file_id = FileId(100);
let file_meta = FileMeta {
id: file_id,
virtual_path: VirtualPath::new("/Test/song.flac"),
real_path: RealPath {
origin_id,
path: PathBuf::from("/song.flac"),
},
size: test_content.len() as u64,
mtime: SystemTime::now(),
content_hash: None,
audio: None,
};
fetcher.register_file(file_meta);
let reader = FileReader::with_fetcher(store, Arc::new(fetcher));
let result = reader
.read(file_id, 0, test_content.len() as u32)
.await
.unwrap();
assert_eq!(&result[..], test_content);
let result2 = reader.read(file_id, 0, 10).await.unwrap();
assert_eq!(&result2[..], &test_content[..10]);
}