From 8ec85d39c55769c564298782a51c6ec25dd6ba9f Mon Sep 17 00:00:00 2001 From: salmon Date: Wed, 12 Aug 2026 02:56:18 +0200 Subject: fix a bug where adding track using `now` when shuffle is on does not work as expected --- tests/queue.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) (limited to 'tests/queue.rs') diff --git a/tests/queue.rs b/tests/queue.rs index d338f2a..67cbabf 100644 --- a/tests/queue.rs +++ b/tests/queue.rs @@ -567,6 +567,83 @@ fn test_shuffle_off_keeps_unplayed_duplicate_entries() { assert_eq!(q.position(), 1); } +#[test] +fn test_shuffle_play_now_current_is_inserted_track() { + // BUG: under shuffle, `insert_after_current` re-shuffles the tail + // (`update_shuffle_indices`), so the inserted track's play-order slot is a + // random draw instead of `position + 1`. The daemon's "now" flow (insert → + // `set_position(pos+1)` → play `track_at(position)`, src/daemon.rs) then + // starts the right file while `current()`/`next()`/the queue display + // follow a *different* track, and auto-advance can replay or skip the + // inserted track entirely. + // + // Contract: after the "now" sequence, `current()` must be the inserted + // track. The seed sweep keeps the test deterministic — for a fixed seed the + // outcome never varies between runs, and pre-fix a given seed satisfies the + // invariant with probability 1/(tail size) = 1/3, so the chance all 40 + // seeds accidentally pass is ~3e-19. The fix makes the invariant hold + // structurally (insert into the play order at `position+1`), independent + // of the seed. + let inserted = PathBuf::from("/music/new.mp3"); + for seed in 1..=40u64 { + let mut q = Queue::with_seed(seed); + add_track(&mut q, "/music/a.mp3"); + add_track(&mut q, "/music/b.mp3"); + add_track(&mut q, "/music/c.mp3"); + q.set_shuffle(ShuffleMode::All); + + // `rsp play --now new.mp3` with a non-empty queue: insert after the + // current track and move the cursor onto it. + q.insert_after_current(inserted.clone()); + let new_pos = q.position() + 1; + q.set_position(new_pos); + + assert_eq!(q.position(), 1); + let cur = q.current().expect("queue is not empty"); + assert_eq!( + cur.path, inserted, + "seed {}: current() after play-now is {:?}, expected {:?}; shuffled_indices={:?}", + seed, + cur.path, + inserted, + q.shuffled_indices() + ); + } +} + +// ---------- Seedable RNG ---------- +// The queue uses a seeded PRNG (rand::rngs::StdRng) instead of the OS-seeded +// thread RNG, so shuffle order is reproducible via Queue::with_seed. Shuffle +// is not a security domain — a PRNG suffices, and deterministic tests need a +// fixed seed. + +#[test] +fn test_same_seed_produces_same_shuffle_order() { + let mut q1 = Queue::with_seed(0x5EED); + let mut q2 = Queue::with_seed(0x5EED); + for i in 0..10 { + add_track(&mut q1, &format!("/music/t{}.mp3", i)); + add_track(&mut q2, &format!("/music/t{}.mp3", i)); + } + q1.set_shuffle(ShuffleMode::All); + q2.set_shuffle(ShuffleMode::All); + assert_eq!(q1.shuffled_indices(), q2.shuffled_indices()); +} + +#[test] +fn test_different_seed_produces_different_shuffle_order() { + let mut q1 = Queue::with_seed(1); + let mut q2 = Queue::with_seed(2); + for i in 0..20 { + add_track(&mut q1, &format!("/music/t{}.mp3", i)); + add_track(&mut q2, &format!("/music/t{}.mp3", i)); + } + q1.set_shuffle(ShuffleMode::All); + q2.set_shuffle(ShuffleMode::All); + // 20! permutations — identical order for two seeds is practically impossible. + assert_ne!(q1.shuffled_indices(), q2.shuffled_indices()); +} + // ---------- Track struct ---------- #[test] -- cgit v1.2.3