From cfd0781ebae76447cc8072fd60883aabf8158d32 Mon Sep 17 00:00:00 2001 From: salmonik Date: Fri, 8 May 2026 00:40:40 +0200 Subject: Extract queue module, add comprehensive tests, fix queue/play bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit • Moved Queue, Track, ShuffleMode, AudioMetadata from daemon.rs into new src/queue.rs module with public API\n• Fixed rsp play interrupting active playback (now checks is_playing)\n• Fixed directory play -n/-i inserting files in reverse order (now uses .iter().rev())\n• Removed dead duplicate shuffle handler in process_command\n• Fixed queue display showing (playing) when playback is stopped\n• Added 34 tests covering navigation, shuffle, insert ordering, edge cases, display --- tests/queue.rs | 459 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 459 insertions(+) create mode 100644 tests/queue.rs (limited to 'tests/queue.rs') diff --git a/tests/queue.rs b/tests/queue.rs new file mode 100644 index 0000000..0ceaba0 --- /dev/null +++ b/tests/queue.rs @@ -0,0 +1,459 @@ +use rsp::queue::{Queue, ShuffleMode, Track}; +use std::path::PathBuf; + +fn track(path: &str, title: Option<&str>, artist: Option<&str>, album: Option<&str>) -> Track { + Track { + path: PathBuf::from(path), + title: title.map(|s| s.to_string()), + artist: artist.map(|s| s.to_string()), + album: album.map(|s| s.to_string()), + } +} + +fn track_no_meta(path: &str) -> Track { + Track { + path: PathBuf::from(path), + title: None, + artist: None, + album: None, + } +} + +fn add_track(q: &mut Queue, path: &str) { + q.add_track(PathBuf::from(path)); +} + +// ---------- Queue basics ---------- + +#[test] +fn test_new_queue_is_empty() { + let q = Queue::new(); + assert!(q.is_empty()); + assert_eq!(q.len(), 0); + assert!(q.current().is_none()); + assert_eq!(q.position(), 0); +} + +#[test] +fn test_add_track_and_current() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + assert!(!q.is_empty()); + assert_eq!(q.len(), 1); + let cur = q.current(); + assert!(cur.is_some()); + assert_eq!(cur.unwrap().path, PathBuf::from("/music/song1.mp3")); +} + +#[test] +fn test_current_on_empty_queue() { + let q = Queue::new(); + assert!(q.current().is_none()); +} + +#[test] +fn test_next_advances_position() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song2.mp3"); + assert_eq!(q.position(), 0); + let next = q.next(); + assert!(next.is_some()); + assert_eq!(next.unwrap(), PathBuf::from("/music/song2.mp3")); + assert_eq!(q.position(), 1); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song2.mp3")); +} + +#[test] +fn test_next_at_end_returns_none() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + assert!(q.next().is_none()); + assert_eq!(q.position(), 0); +} + +#[test] +fn test_next_from_last_track_returns_none() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song2.mp3"); + assert!(q.next().is_some()); + assert!(q.next().is_none()); + assert_eq!(q.position(), 1); +} + +#[test] +fn test_previous_goes_back() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song2.mp3"); + add_track(&mut q, "/music/song3.mp3"); + q.next(); + q.next(); + assert_eq!(q.position(), 2); + let prev = q.previous(); + assert!(prev.is_some()); + assert_eq!(prev.unwrap(), PathBuf::from("/music/song2.mp3")); + assert_eq!(q.position(), 1); +} + +#[test] +fn test_previous_at_start_returns_none() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + assert!(q.previous().is_none()); + assert_eq!(q.position(), 0); +} + +#[test] +fn test_previous_multiple_steps() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song2.mp3"); + add_track(&mut q, "/music/song3.mp3"); + q.next(); + q.next(); + q.previous(); + assert_eq!(q.position(), 1); + q.previous(); + assert_eq!(q.position(), 0); + assert!(q.previous().is_none()); + assert_eq!(q.position(), 0); +} + +// ---------- Clear ---------- + +#[test] +fn test_clear_queue() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song2.mp3"); + q.next(); + q.clear(); + assert!(q.is_empty()); + assert_eq!(q.len(), 0); + assert_eq!(q.position(), 0); + assert!(q.current().is_none()); +} + +#[test] +fn test_clear_empty_queue() { + let mut q = Queue::new(); + q.clear(); + assert!(q.is_empty()); +} + +// ---------- Insert after current ---------- + +#[test] +fn test_insert_after_current_empty_queue() { + let mut q = Queue::new(); + q.insert_after_current(PathBuf::from("/music/song1.mp3")); + assert_eq!(q.len(), 1); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song1.mp3")); +} + +#[test] +fn test_insert_after_current_middle() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song3.mp3"); + // Currently at position 0 (song1) + q.insert_after_current(PathBuf::from("/music/song2.mp3")); + assert_eq!(q.len(), 3); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song1.mp3")); + q.next(); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song2.mp3")); + q.next(); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song3.mp3")); +} + +#[test] +fn test_insert_after_current_at_end() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song2.mp3"); + q.next(); + q.insert_after_current(PathBuf::from("/music/song3.mp3")); + assert_eq!(q.len(), 3); + q.next(); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song3.mp3")); +} + +#[test] +fn test_insert_after_current_multiple_ordering() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song4.mp3"); + // Queue: [song1, song4], pos=0 + // Insert song2, then song3 after current + q.insert_after_current(PathBuf::from("/music/song2.mp3")); + q.insert_after_current(PathBuf::from("/music/song3.mp3")); + // Expected: [song1, song3, song2, song4] + // Each insert goes at pos+1, so song3 ends up right after song1, pushing song2 right + assert_eq!(q.len(), 4); + q.next(); + assert_eq!( + q.current().unwrap().path, + PathBuf::from("/music/song3.mp3"), + "last insert goes right after current" + ); + q.next(); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song2.mp3")); + q.next(); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song4.mp3")); +} + +// ---------- Track display ---------- + +#[test] +fn test_track_display_with_metadata() { + let t = track( + "/music/test.mp3", + Some("Test Title"), + Some("Test Artist"), + Some("Test Album"), + ); + let display = t.display_string(0); + assert_eq!(display, "1. Test Artist - Test Title"); +} + +#[test] +fn test_track_display_artist_fallback_to_album() { + let t = track( + "/music/test.mp3", + Some("Test Title"), + None, + Some("Test Album"), + ); + let display = t.display_string(0); + assert_eq!(display, "1. Test Album - Test Title"); +} + +#[test] +fn test_track_display_no_metadata() { + let t = track_no_meta("/music/song.mp3"); + let display = t.display_string(0); + assert_eq!(display, "1. song.mp3"); +} + +#[test] +fn test_track_display_index_offset() { + let t = track( + "/music/test.mp3", + Some("Title"), + Some("Artist"), + Some("Album"), + ); + assert_eq!(t.display_string(4), "5. Artist - Title"); +} + +// ---------- Shuffle ---------- + +#[test] +fn test_shuffle_modes() { + let mut q = Queue::new(); + assert_eq!(*q.shuffle_mode(), ShuffleMode::None); + q.set_shuffle(ShuffleMode::All); + assert_eq!(*q.shuffle_mode(), ShuffleMode::All); + q.set_shuffle(ShuffleMode::Album); + assert_eq!(*q.shuffle_mode(), ShuffleMode::Album); + q.set_shuffle(ShuffleMode::None); + assert_eq!(*q.shuffle_mode(), ShuffleMode::None); +} + +#[test] +fn test_shuffle_all_navigates_in_shuffled_order() { + let mut q = Queue::new(); + for i in 0..10 { + add_track(&mut q, &format!("/music/song{}.mp3", i)); + } + q.set_shuffle(ShuffleMode::All); + // Current should still return the first track + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song0.mp3")); + // Navigate through all tracks, should visit all 10 (in shuffled order) + let mut visited = std::collections::HashSet::new(); + visited.insert(q.current().unwrap().path.clone()); + while let Some(next_path) = q.next() { + assert!( + visited.insert(next_path), + "duplicate track in shuffle navigation" + ); + } + assert_eq!(visited.len(), 10, "should visit all 10 tracks"); +} + +#[test] +fn test_shuffle_all_current_stays_at_position_zero() { + let mut q = Queue::new(); + for i in 0..5 { + add_track(&mut q, &format!("/music/song{}.mp3", i)); + } + q.set_shuffle(ShuffleMode::All); + assert_eq!(q.position(), 0); + assert_eq!(q.shuffled_indices()[0], 0); +} + +#[test] +fn test_set_shuffle_regenerates_indices() { + let mut q = Queue::new(); + for i in 0..5 { + add_track(&mut q, &format!("/music/song{}.mp3", i)); + } + q.set_shuffle(ShuffleMode::All); + let indices1 = q.shuffled_indices().to_vec(); + // Each set_shuffle should regenerate (small chance of same order) + q.set_shuffle(ShuffleMode::All); + let indices2 = q.shuffled_indices().to_vec(); + // Check that all indices are still valid (0..5) + let mut sorted1 = indices1.clone(); + sorted1.sort(); + let mut sorted2 = indices2.clone(); + sorted2.sort(); + assert_eq!(sorted1, vec![0, 1, 2, 3, 4]); + assert_eq!(sorted2, vec![0, 1, 2, 3, 4]); +} + +#[test] +fn test_shuffle_none_linear_navigation() { + let mut q = Queue::new(); + for i in 0..5 { + add_track(&mut q, &format!("/music/song{}.mp3", i)); + } + assert_eq!(*q.shuffle_mode(), ShuffleMode::None); + assert_eq!(q.shuffled_indices(), &[0, 1, 2, 3, 4]); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song0.mp3")); + q.next(); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song1.mp3")); + q.next(); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song2.mp3")); +} + +// ---------- Edge cases ---------- + +#[test] +fn test_single_track_queue() { + let mut q = Queue::new(); + add_track(&mut q, "/music/only.mp3"); + assert_eq!(q.len(), 1); + assert!(q.current().is_some()); + assert!(q.next().is_none()); + assert_eq!(q.position(), 0); + assert!(q.previous().is_none()); + assert_eq!(q.position(), 0); +} + +#[test] +fn test_set_position() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song0.mp3"); + add_track(&mut q, "/music/song1.mp3"); + add_track(&mut q, "/music/song2.mp3"); + q.set_position(2); + assert_eq!(q.position(), 2); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song2.mp3")); + q.set_position(0); + assert_eq!(q.position(), 0); + assert_eq!(q.current().unwrap().path, PathBuf::from("/music/song0.mp3")); +} + +#[test] +fn test_set_position_beyond_end() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song0.mp3"); + add_track(&mut q, "/music/song1.mp3"); + q.set_position(5); + // current() should handle out-of-bounds position gracefully + let cur = q.current(); + assert!(cur.is_none()); + assert!(q.next().is_none()); +} + +#[test] +fn test_len_and_is_empty() { + let mut q = Queue::new(); + assert_eq!(q.len(), 0); + assert!(q.is_empty()); + add_track(&mut q, "/music/a.mp3"); + assert_eq!(q.len(), 1); + assert!(!q.is_empty()); + add_track(&mut q, "/music/b.mp3"); + assert_eq!(q.len(), 2); + q.clear(); + assert_eq!(q.len(), 0); + assert!(q.is_empty()); +} + +#[test] +fn test_track_at() { + let mut q = Queue::new(); + add_track(&mut q, "/music/song0.mp3"); + add_track(&mut q, "/music/song1.mp3"); + assert_eq!( + q.track_at(0).unwrap().path, + PathBuf::from("/music/song0.mp3") + ); + assert_eq!( + q.track_at(1).unwrap().path, + PathBuf::from("/music/song1.mp3") + ); + assert!(q.track_at(2).is_none()); +} + +// ---------- Shuffle + add interaction ---------- + +#[test] +fn test_add_track_updates_shuffle_indices() { + let mut q = Queue::new(); + q.set_shuffle(ShuffleMode::All); + add_track(&mut q, "/music/a.mp3"); + assert_eq!(q.shuffled_indices().len(), 1); + add_track(&mut q, "/music/b.mp3"); + assert_eq!(q.shuffled_indices().len(), 2); + add_track(&mut q, "/music/c.mp3"); + assert_eq!(q.shuffled_indices().len(), 3); +} + +#[test] +fn test_insert_after_current_updates_shuffle_indices() { + let mut q = Queue::new(); + q.set_shuffle(ShuffleMode::All); + add_track(&mut q, "/music/a.mp3"); + q.insert_after_current(PathBuf::from("/music/b.mp3")); + assert_eq!(q.shuffled_indices().len(), 2); +} + +#[test] +fn test_clear_resets_shuffle_indices() { + let mut q = Queue::new(); + q.set_shuffle(ShuffleMode::All); + add_track(&mut q, "/music/a.mp3"); + add_track(&mut q, "/music/b.mp3"); + q.clear(); + assert!(q.shuffled_indices().is_empty()); +} + +// ---------- Track struct ---------- + +#[test] +fn test_track_fields() { + let t = track( + "/path/to/file.flac", + Some("Title"), + Some("Artist"), + Some("Album"), + ); + assert_eq!(t.path, PathBuf::from("/path/to/file.flac")); + assert_eq!(t.title.unwrap(), "Title"); + assert_eq!(t.artist.unwrap(), "Artist"); + assert_eq!(t.album.unwrap(), "Album"); +} + +#[test] +fn test_track_no_metadata_fields() { + let t = track_no_meta("/path/to/file.flac"); + assert_eq!(t.path, PathBuf::from("/path/to/file.flac")); + assert!(t.title.is_none()); + assert!(t.artist.is_none()); + assert!(t.album.is_none()); +} -- cgit v1.2.3