//! Tests for the direct backend. use super::*; fn setup() -> DirectBackend { let dir = tempfile::TempDir::new().unwrap(); let db = Database::open_in_memory().unwrap(); let store = SampleStore::new(dir.path().join("store")).unwrap(); DirectBackend::new(db, store, dir.path().to_path_buf()) } #[test] fn list_vfs_empty_then_create() { let backend = setup(); // Fresh DB has no VFS let vfs_list = backend.list_vfs().unwrap(); assert!(vfs_list.is_empty()); let id = backend.create_vfs("Library").unwrap(); assert!(id.as_i64() > 0); let vfs_list = backend.list_vfs().unwrap(); assert_eq!(vfs_list.len(), 1); assert_eq!(vfs_list[0].name, "Library"); } #[test] fn directory_crud() { let backend = setup(); let vfs_id = backend.create_vfs("Test").unwrap(); let dir_id = backend.create_directory(vfs_id, None, "Drums").unwrap(); let children = backend.list_children_enriched(vfs_id, None).unwrap(); assert_eq!(children.len(), 1); assert_eq!(children[0].node.name, "Drums"); backend.rename_node(dir_id, "Percussion").unwrap(); let node = backend.get_node(dir_id).unwrap(); assert_eq!(node.name, "Percussion"); backend.delete_node(dir_id).unwrap(); let children = backend.list_children_enriched(vfs_id, None).unwrap(); assert!(children.is_empty()); } /// Insert a fake sample row for testing (no actual file on disk). fn insert_fake_sample(db: &Database, hash: &str) { db.conn() .execute( "INSERT OR IGNORE INTO samples (hash, original_name, file_extension, file_size, import_date, last_modified) VALUES (?1, ?2, 'wav', 100, 0, 0)", rusqlite::params![hash, format!("{hash}.wav")], ) .unwrap(); } #[test] fn tags_crud() { let backend = setup(); // Insert a fake sample row for tag FK { let db = backend.db.lock(); insert_fake_sample(&db, "testhash"); } backend.add_tag("testhash", "drums.kick").unwrap(); let tags = backend.get_sample_tags("testhash").unwrap(); assert_eq!(tags, vec!["drums.kick"]); backend.remove_tag("testhash", "drums.kick").unwrap(); let tags = backend.get_sample_tags("testhash").unwrap(); assert!(tags.is_empty()); } #[test] fn config_crud() { let backend = setup(); assert!( backend .get_config(crate::backend::ConfigKey::Theme) .unwrap() .is_none() ); backend .set_config(crate::backend::ConfigKey::Theme, "dark") .unwrap(); assert_eq!( backend .get_config(crate::backend::ConfigKey::Theme) .unwrap() .unwrap(), "dark" ); backend .set_config(crate::backend::ConfigKey::Theme, "light") .unwrap(); assert_eq!( backend .get_config(crate::backend::ConfigKey::Theme) .unwrap() .unwrap(), "light" ); } #[test] fn dynamic_collection_crud() { let backend = setup(); let filter = SearchFilter::default(); let id = backend.create_dynamic_collection("Kicks", &filter).unwrap(); assert!(id.as_i64() > 0); let colls = backend.list_collections().unwrap(); let dynamic = colls.iter().find(|c| c.name == "Kicks").unwrap(); assert!(dynamic.is_dynamic()); } #[test] fn search_empty_filter_returns_all() { let backend = setup(); let vfs_id = backend.create_vfs("Test").unwrap(); // Insert fake samples so search has something to find { let db = backend.db.lock(); insert_fake_sample(&db, "h1"); insert_fake_sample(&db, "h2"); vfs::create_sample_link(&db, vfs_id, None, "kick.wav", "h1").unwrap(); vfs::create_sample_link(&db, vfs_id, None, "snare.wav", "h2").unwrap(); } let filter = SearchFilter::default(); let results = backend.search_in_folder(&filter, vfs_id, None).unwrap(); assert_eq!(results.len(), 2); } #[test] fn poll_events_empty_when_no_workers() { let backend = setup(); let events = backend.poll_events(); assert!(events.is_empty()); } #[test] fn breadcrumb_and_subtree() { let backend = setup(); let vfs_id = backend.create_vfs("Test").unwrap(); let a = backend.create_directory(vfs_id, None, "A").unwrap(); let b = backend.create_directory(vfs_id, Some(a), "B").unwrap(); let crumbs = backend.get_breadcrumb(b).unwrap(); assert_eq!(crumbs.len(), 2); assert_eq!(crumbs[0].name, "A"); assert_eq!(crumbs[1].name, "B"); let subtree = backend.collect_subtree(a).unwrap(); assert_eq!(subtree.len(), 2); } #[test] fn list_all_directories_works() { let backend = setup(); let vfs_id = backend.create_vfs("Test").unwrap(); let drums = backend.create_directory(vfs_id, None, "Drums").unwrap(); backend .create_directory(vfs_id, Some(drums), "Kicks") .unwrap(); let dirs = backend.list_all_directories(vfs_id).unwrap(); assert_eq!(dirs.len(), 2); } /// Write a minimal float-PCM WAV for forge integration tests. fn write_float_wav(path: &Path, channels: u16, sample_rate: u32, samples: &[f32]) { use std::io::Write; let bytes_per_sample = 4u16; let block_align = channels * bytes_per_sample; let data_size = (samples.len() as u32) * 4; let file_size = 36 + data_size; let mut buf = Vec::with_capacity(44 + data_size as usize); buf.extend_from_slice(b"RIFF"); buf.extend_from_slice(&file_size.to_le_bytes()); buf.extend_from_slice(b"WAVE"); buf.extend_from_slice(b"fmt "); buf.extend_from_slice(&16u32.to_le_bytes()); buf.extend_from_slice(&3u16.to_le_bytes()); buf.extend_from_slice(&channels.to_le_bytes()); buf.extend_from_slice(&sample_rate.to_le_bytes()); buf.extend_from_slice(&(sample_rate * block_align as u32).to_le_bytes()); buf.extend_from_slice(&block_align.to_le_bytes()); buf.extend_from_slice(&(bytes_per_sample * 8).to_le_bytes()); buf.extend_from_slice(b"data"); buf.extend_from_slice(&data_size.to_le_bytes()); for &s in samples { buf.extend_from_slice(&s.to_le_bytes()); } std::fs::File::create(path) .unwrap() .write_all(&buf) .unwrap(); } #[test] fn chop_sample_creates_slice_folder() { use audiofiles_core::forge::ChopMethod; // Keep the temp dir alive for the whole test (store lives under it). let dir = tempfile::TempDir::new().unwrap(); let db = Database::open_in_memory().unwrap(); let store = SampleStore::new(dir.path().join("store")).unwrap(); let backend = DirectBackend::new(db, store, dir.path().to_path_buf()); let vfs_id = backend.create_vfs("Test").unwrap(); let samples: Vec = (0..2000).map(|i| ((i % 40) as f32 / 40.0) - 0.5).collect(); let src = dir.path().join("loop.wav"); write_float_wav(&src, 1, 44100, &samples); let hash = backend.import_file(&src).unwrap(); // Preview computes slice boundaries on the forge worker (N starts + // trailing 1.0), delivered via poll_events. backend .start_chop_preview(&hash, "wav", &ChopMethod::EqualDivisions(4)) .unwrap(); let marks = loop { let mut found = None; for ev in backend.poll_events() { if let BackendEvent::ChopPreviewComplete { marks } = ev { found = Some(marks); } } if let Some(m) = found { break m; } std::thread::yield_now(); }; assert_eq!(marks.len(), 5); assert_eq!(marks.last().copied(), Some(1.0)); let count = backend .chop_sample( vfs_id, &hash, "wav", "loop.wav", None, &ChopMethod::EqualDivisions(4), ) .unwrap(); assert_eq!(count, 4); // A "loop_slices" directory now holds 4 samples. let roots = backend.list_children(vfs_id, None).unwrap(); let slice_dir = roots.iter().find(|n| n.name == "loop_slices").unwrap(); let slices = backend.list_children(vfs_id, Some(slice_dir.id)).unwrap(); assert_eq!(slices.len(), 4); } #[test] fn start_chop_runs_on_worker_and_imports_on_poll() { use audiofiles_core::forge::ChopMethod; let dir = tempfile::TempDir::new().unwrap(); // File-based db: the off-thread forge import opens its own connection from // data_dir/audiofiles.db (the same pattern every worker uses), so the test // backend must share that file rather than an in-memory db. let db = Database::open(dir.path().join("audiofiles.db")).unwrap(); let store = SampleStore::new(dir.path().join("store")).unwrap(); let backend = DirectBackend::new(db, store, dir.path().to_path_buf()); let vfs_id = backend.create_vfs("Test").unwrap(); let samples: Vec = (0..2000).map(|i| ((i % 40) as f32 / 40.0) - 0.5).collect(); let src = dir.path().join("loop.wav"); write_float_wav(&src, 1, 44100, &samples); let hash = backend.import_file(&src).unwrap(); // Dispatch to the worker; returns immediately (no UI block). backend .start_chop( vfs_id, &hash, "wav", "loop.wav", None, &ChopMethod::EqualDivisions(4), ) .unwrap(); // Poll until the chop completes (worker does CPU; poll imports). let mut slice_count = None; for _ in 0..200 { for ev in backend.poll_events() { match ev { BackendEvent::ForgeChopComplete { slice_count: n } => slice_count = Some(n), BackendEvent::ForgeError { error } => panic!("forge error: {error}"), _ => {} } } if slice_count.is_some() { break; } std::thread::sleep(std::time::Duration::from_millis(10)); } assert_eq!(slice_count, Some(4), "chop should report 4 slices"); // The slices landed in the VFS under "loop_slices". let roots = backend.list_children(vfs_id, None).unwrap(); let slice_dir = roots.iter().find(|n| n.name == "loop_slices").unwrap(); let slices = backend.list_children(vfs_id, Some(slice_dir.id)).unwrap(); assert_eq!(slices.len(), 4); } #[test] #[cfg(feature = "device-profiles")] fn device_conform_target_resolves_bundled() { let backend = setup(); // A bundled mono device resolves to a mono target. let target = backend.device_conform_target("SP-404 MKII", 48000).unwrap(); assert!(target.is_some(), "SP-404 MKII should resolve to a target"); } #[test] #[cfg(feature = "device-profiles")] fn list_device_profiles_returns_bundled() { let backend = setup(); let profiles = backend.list_device_profiles().unwrap(); // Bundled manifests should be loaded (14 devices) assert!( profiles.len() >= 14, "expected at least 14 bundled profiles, got {}", profiles.len() ); // Spot-check a known device assert!( profiles.iter().any(|p| p.name == "SP-404 MKII"), "SP-404 MKII should be in the bundled profiles" ); }