Skip to main content

max / audiofiles

10.8 KB · 338 lines History Blame Raw
1 //! Tests for the direct backend.
2
3 use super::*;
4
5 fn setup() -> DirectBackend {
6 let dir = tempfile::TempDir::new().unwrap();
7 let db = Database::open_in_memory().unwrap();
8 let store = SampleStore::new(dir.path().join("store")).unwrap();
9 DirectBackend::new(db, store, dir.path().to_path_buf())
10 }
11
12 #[test]
13 fn list_vfs_empty_then_create() {
14 let backend = setup();
15 // Fresh DB has no VFS
16 let vfs_list = backend.list_vfs().unwrap();
17 assert!(vfs_list.is_empty());
18
19 let id = backend.create_vfs("Library").unwrap();
20 assert!(id.as_i64() > 0);
21
22 let vfs_list = backend.list_vfs().unwrap();
23 assert_eq!(vfs_list.len(), 1);
24 assert_eq!(vfs_list[0].name, "Library");
25 }
26
27 #[test]
28 fn directory_crud() {
29 let backend = setup();
30 let vfs_id = backend.create_vfs("Test").unwrap();
31 let dir_id = backend.create_directory(vfs_id, None, "Drums").unwrap();
32
33 let children = backend.list_children_enriched(vfs_id, None).unwrap();
34 assert_eq!(children.len(), 1);
35 assert_eq!(children[0].node.name, "Drums");
36
37 backend.rename_node(dir_id, "Percussion").unwrap();
38 let node = backend.get_node(dir_id).unwrap();
39 assert_eq!(node.name, "Percussion");
40
41 backend.delete_node(dir_id).unwrap();
42 let children = backend.list_children_enriched(vfs_id, None).unwrap();
43 assert!(children.is_empty());
44 }
45
46 /// Insert a fake sample row for testing (no actual file on disk).
47 fn insert_fake_sample(db: &Database, hash: &str) {
48 db.conn()
49 .execute(
50 "INSERT OR IGNORE INTO samples (hash, original_name, file_extension, file_size, import_date, last_modified)
51 VALUES (?1, ?2, 'wav', 100, 0, 0)",
52 rusqlite::params![hash, format!("{hash}.wav")],
53 )
54 .unwrap();
55 }
56
57 #[test]
58 fn tags_crud() {
59 let backend = setup();
60 // Insert a fake sample row for tag FK
61 {
62 let db = backend.db.lock();
63 insert_fake_sample(&db, "testhash");
64 }
65
66 backend.add_tag("testhash", "drums.kick").unwrap();
67 let tags = backend.get_sample_tags("testhash").unwrap();
68 assert_eq!(tags, vec!["drums.kick"]);
69
70 backend.remove_tag("testhash", "drums.kick").unwrap();
71 let tags = backend.get_sample_tags("testhash").unwrap();
72 assert!(tags.is_empty());
73 }
74
75 #[test]
76 fn config_crud() {
77 let backend = setup();
78 assert!(
79 backend
80 .get_config(crate::backend::ConfigKey::Theme)
81 .unwrap()
82 .is_none()
83 );
84
85 backend
86 .set_config(crate::backend::ConfigKey::Theme, "dark")
87 .unwrap();
88 assert_eq!(
89 backend
90 .get_config(crate::backend::ConfigKey::Theme)
91 .unwrap()
92 .unwrap(),
93 "dark"
94 );
95
96 backend
97 .set_config(crate::backend::ConfigKey::Theme, "light")
98 .unwrap();
99 assert_eq!(
100 backend
101 .get_config(crate::backend::ConfigKey::Theme)
102 .unwrap()
103 .unwrap(),
104 "light"
105 );
106 }
107
108 #[test]
109 fn dynamic_collection_crud() {
110 let backend = setup();
111 let filter = SearchFilter::default();
112
113 let id = backend.create_dynamic_collection("Kicks", &filter).unwrap();
114 assert!(id.as_i64() > 0);
115
116 let colls = backend.list_collections().unwrap();
117 let dynamic = colls.iter().find(|c| c.name == "Kicks").unwrap();
118 assert!(dynamic.is_dynamic());
119 }
120
121 #[test]
122 fn search_empty_filter_returns_all() {
123 let backend = setup();
124 let vfs_id = backend.create_vfs("Test").unwrap();
125 // Insert fake samples so search has something to find
126 {
127 let db = backend.db.lock();
128 insert_fake_sample(&db, "h1");
129 insert_fake_sample(&db, "h2");
130 vfs::create_sample_link(&db, vfs_id, None, "kick.wav", "h1").unwrap();
131 vfs::create_sample_link(&db, vfs_id, None, "snare.wav", "h2").unwrap();
132 }
133
134 let filter = SearchFilter::default();
135 let results = backend.search_in_folder(&filter, vfs_id, None).unwrap();
136 assert_eq!(results.len(), 2);
137 }
138
139 #[test]
140 fn poll_events_empty_when_no_workers() {
141 let backend = setup();
142 let events = backend.poll_events();
143 assert!(events.is_empty());
144 }
145
146 #[test]
147 fn breadcrumb_and_subtree() {
148 let backend = setup();
149 let vfs_id = backend.create_vfs("Test").unwrap();
150 let a = backend.create_directory(vfs_id, None, "A").unwrap();
151 let b = backend.create_directory(vfs_id, Some(a), "B").unwrap();
152
153 let crumbs = backend.get_breadcrumb(b).unwrap();
154 assert_eq!(crumbs.len(), 2);
155 assert_eq!(crumbs[0].name, "A");
156 assert_eq!(crumbs[1].name, "B");
157
158 let subtree = backend.collect_subtree(a).unwrap();
159 assert_eq!(subtree.len(), 2);
160 }
161
162 #[test]
163 fn list_all_directories_works() {
164 let backend = setup();
165 let vfs_id = backend.create_vfs("Test").unwrap();
166 let drums = backend.create_directory(vfs_id, None, "Drums").unwrap();
167 backend
168 .create_directory(vfs_id, Some(drums), "Kicks")
169 .unwrap();
170
171 let dirs = backend.list_all_directories(vfs_id).unwrap();
172 assert_eq!(dirs.len(), 2);
173 }
174
175 /// Write a minimal float-PCM WAV for forge integration tests.
176 fn write_float_wav(path: &Path, channels: u16, sample_rate: u32, samples: &[f32]) {
177 use std::io::Write;
178 let bytes_per_sample = 4u16;
179 let block_align = channels * bytes_per_sample;
180 let data_size = (samples.len() as u32) * 4;
181 let file_size = 36 + data_size;
182 let mut buf = Vec::with_capacity(44 + data_size as usize);
183 buf.extend_from_slice(b"RIFF");
184 buf.extend_from_slice(&file_size.to_le_bytes());
185 buf.extend_from_slice(b"WAVE");
186 buf.extend_from_slice(b"fmt ");
187 buf.extend_from_slice(&16u32.to_le_bytes());
188 buf.extend_from_slice(&3u16.to_le_bytes());
189 buf.extend_from_slice(&channels.to_le_bytes());
190 buf.extend_from_slice(&sample_rate.to_le_bytes());
191 buf.extend_from_slice(&(sample_rate * block_align as u32).to_le_bytes());
192 buf.extend_from_slice(&block_align.to_le_bytes());
193 buf.extend_from_slice(&(bytes_per_sample * 8).to_le_bytes());
194 buf.extend_from_slice(b"data");
195 buf.extend_from_slice(&data_size.to_le_bytes());
196 for &s in samples {
197 buf.extend_from_slice(&s.to_le_bytes());
198 }
199 std::fs::File::create(path)
200 .unwrap()
201 .write_all(&buf)
202 .unwrap();
203 }
204
205 #[test]
206 fn chop_sample_creates_slice_folder() {
207 use audiofiles_core::forge::ChopMethod;
208 // Keep the temp dir alive for the whole test (store lives under it).
209 let dir = tempfile::TempDir::new().unwrap();
210 let db = Database::open_in_memory().unwrap();
211 let store = SampleStore::new(dir.path().join("store")).unwrap();
212 let backend = DirectBackend::new(db, store, dir.path().to_path_buf());
213
214 let vfs_id = backend.create_vfs("Test").unwrap();
215 let samples: Vec<f32> = (0..2000).map(|i| ((i % 40) as f32 / 40.0) - 0.5).collect();
216 let src = dir.path().join("loop.wav");
217 write_float_wav(&src, 1, 44100, &samples);
218 let hash = backend.import_file(&src).unwrap();
219
220 // Preview computes slice boundaries on the forge worker (N starts +
221 // trailing 1.0), delivered via poll_events.
222 backend
223 .start_chop_preview(&hash, "wav", &ChopMethod::EqualDivisions(4))
224 .unwrap();
225 let marks = loop {
226 let mut found = None;
227 for ev in backend.poll_events() {
228 if let BackendEvent::ChopPreviewComplete { marks } = ev {
229 found = Some(marks);
230 }
231 }
232 if let Some(m) = found {
233 break m;
234 }
235 std::thread::yield_now();
236 };
237 assert_eq!(marks.len(), 5);
238 assert_eq!(marks.last().copied(), Some(1.0));
239
240 let count = backend
241 .chop_sample(
242 vfs_id,
243 &hash,
244 "wav",
245 "loop.wav",
246 None,
247 &ChopMethod::EqualDivisions(4),
248 )
249 .unwrap();
250 assert_eq!(count, 4);
251
252 // A "loop_slices" directory now holds 4 samples.
253 let roots = backend.list_children(vfs_id, None).unwrap();
254 let slice_dir = roots.iter().find(|n| n.name == "loop_slices").unwrap();
255 let slices = backend.list_children(vfs_id, Some(slice_dir.id)).unwrap();
256 assert_eq!(slices.len(), 4);
257 }
258
259 #[test]
260 fn start_chop_runs_on_worker_and_imports_on_poll() {
261 use audiofiles_core::forge::ChopMethod;
262 let dir = tempfile::TempDir::new().unwrap();
263 // File-based db: the off-thread forge import opens its own connection from
264 // data_dir/audiofiles.db (the same pattern every worker uses), so the test
265 // backend must share that file rather than an in-memory db.
266 let db = Database::open(dir.path().join("audiofiles.db")).unwrap();
267 let store = SampleStore::new(dir.path().join("store")).unwrap();
268 let backend = DirectBackend::new(db, store, dir.path().to_path_buf());
269
270 let vfs_id = backend.create_vfs("Test").unwrap();
271 let samples: Vec<f32> = (0..2000).map(|i| ((i % 40) as f32 / 40.0) - 0.5).collect();
272 let src = dir.path().join("loop.wav");
273 write_float_wav(&src, 1, 44100, &samples);
274 let hash = backend.import_file(&src).unwrap();
275
276 // Dispatch to the worker; returns immediately (no UI block).
277 backend
278 .start_chop(
279 vfs_id,
280 &hash,
281 "wav",
282 "loop.wav",
283 None,
284 &ChopMethod::EqualDivisions(4),
285 )
286 .unwrap();
287
288 // Poll until the chop completes (worker does CPU; poll imports).
289 let mut slice_count = None;
290 for _ in 0..200 {
291 for ev in backend.poll_events() {
292 match ev {
293 BackendEvent::ForgeChopComplete { slice_count: n } => slice_count = Some(n),
294 BackendEvent::ForgeError { error } => panic!("forge error: {error}"),
295 _ => {}
296 }
297 }
298 if slice_count.is_some() {
299 break;
300 }
301 std::thread::sleep(std::time::Duration::from_millis(10));
302 }
303 assert_eq!(slice_count, Some(4), "chop should report 4 slices");
304
305 // The slices landed in the VFS under "loop_slices".
306 let roots = backend.list_children(vfs_id, None).unwrap();
307 let slice_dir = roots.iter().find(|n| n.name == "loop_slices").unwrap();
308 let slices = backend.list_children(vfs_id, Some(slice_dir.id)).unwrap();
309 assert_eq!(slices.len(), 4);
310 }
311
312 #[test]
313 #[cfg(feature = "device-profiles")]
314 fn device_conform_target_resolves_bundled() {
315 let backend = setup();
316 // A bundled mono device resolves to a mono target.
317 let target = backend.device_conform_target("SP-404 MKII", 48000).unwrap();
318 assert!(target.is_some(), "SP-404 MKII should resolve to a target");
319 }
320
321 #[test]
322 #[cfg(feature = "device-profiles")]
323 fn list_device_profiles_returns_bundled() {
324 let backend = setup();
325 let profiles = backend.list_device_profiles().unwrap();
326 // Bundled manifests should be loaded (14 devices)
327 assert!(
328 profiles.len() >= 14,
329 "expected at least 14 bundled profiles, got {}",
330 profiles.len()
331 );
332 // Spot-check a known device
333 assert!(
334 profiles.iter().any(|p| p.name == "SP-404 MKII"),
335 "SP-404 MKII should be in the bundled profiles"
336 );
337 }
338