diff options
| author | salmon <salmonik@tuta.io> | 2026-08-12 02:56:18 +0200 |
|---|---|---|
| committer | salmon <salmonik@tuta.io> | 2026-08-12 02:56:18 +0200 |
| commit | 8ec85d39c55769c564298782a51c6ec25dd6ba9f (patch) | |
| tree | a55bda29f2f12afbb64eb5ef05a920fa19ca4d3e | |
| parent | clean up readme and make install.sh activate rc.d service (diff) | |
| download | rsplayer-8ec85d39c55769c564298782a51c6ec25dd6ba9f.tar.gz rsplayer-8ec85d39c55769c564298782a51c6ec25dd6ba9f.zip | |
fix a bug where adding track using `now` when shuffle is on does not work as expected
| -rw-r--r-- | src/daemon.rs | 14 | ||||
| -rw-r--r-- | src/queue.rs | 87 | ||||
| -rw-r--r-- | tests/queue.rs | 77 |
3 files changed, 149 insertions, 29 deletions
diff --git a/src/daemon.rs b/src/daemon.rs index 21f1a3b..bdee362 100644 --- a/src/daemon.rs +++ b/src/daemon.rs | |||
| @@ -1075,7 +1075,7 @@ fn process_command( | |||
| 1075 | let new_pos = q.position() + 1; | 1075 | let new_pos = q.position() + 1; |
| 1076 | q.set_position(new_pos); | 1076 | q.set_position(new_pos); |
| 1077 | } | 1077 | } |
| 1078 | if let Some(track) = q.track_at(q.position()) { | 1078 | if let Some(track) = q.current() { |
| 1079 | audio.play(track.path.clone()); | 1079 | audio.play(track.path.clone()); |
| 1080 | spawn_send_now_playing(scrobbler, track); | 1080 | spawn_send_now_playing(scrobbler, track); |
| 1081 | if let Ok(mut s) = scrobble_state.lock() { | 1081 | if let Ok(mut s) = scrobble_state.lock() { |
| @@ -1181,12 +1181,12 @@ fn process_command( | |||
| 1181 | match position { | 1181 | match position { |
| 1182 | "now" => { | 1182 | "now" => { |
| 1183 | spawn_try_scrobble_current(scrobbler, scrobble_state); | 1183 | spawn_try_scrobble_current(scrobbler, scrobble_state); |
| 1184 | q.push_track(track.clone()); | 1184 | q.insert_after_current(PathBuf::from(path_str.clone())); |
| 1185 | if !was_empty { | 1185 | if !was_empty { |
| 1186 | let new_pos = q.position() + 1; | 1186 | let new_pos = q.position() + 1; |
| 1187 | q.set_position(new_pos); | 1187 | q.set_position(new_pos); |
| 1188 | } | 1188 | } |
| 1189 | if let Some(track) = q.track_at(q.position()) { | 1189 | if let Some(track) = q.current() { |
| 1190 | audio.play(track.path.clone()); | 1190 | audio.play(track.path.clone()); |
| 1191 | spawn_send_now_playing(scrobbler, track); | 1191 | spawn_send_now_playing(scrobbler, track); |
| 1192 | if let Ok(mut s) = scrobble_state.lock() { | 1192 | if let Ok(mut s) = scrobble_state.lock() { |
| @@ -1199,7 +1199,7 @@ fn process_command( | |||
| 1199 | format!("Playing now: {}\n", path_str) | 1199 | format!("Playing now: {}\n", path_str) |
| 1200 | } | 1200 | } |
| 1201 | "next" => { | 1201 | "next" => { |
| 1202 | q.push_track(track); | 1202 | q.insert_after_current(PathBuf::from(path_str.clone())); |
| 1203 | format!("Queued next: {}\n", path_str) | 1203 | format!("Queued next: {}\n", path_str) |
| 1204 | } | 1204 | } |
| 1205 | _ => { | 1205 | _ => { |
| @@ -1237,7 +1237,7 @@ fn process_command( | |||
| 1237 | let new_pos = q.position() + 1; | 1237 | let new_pos = q.position() + 1; |
| 1238 | q.set_position(new_pos); | 1238 | q.set_position(new_pos); |
| 1239 | } | 1239 | } |
| 1240 | if let Some(track) = q.track_at(q.position()) { | 1240 | if let Some(track) = q.current() { |
| 1241 | audio.play(track.path.clone()); | 1241 | audio.play(track.path.clone()); |
| 1242 | spawn_send_now_playing(scrobbler, track); | 1242 | spawn_send_now_playing(scrobbler, track); |
| 1243 | if let Ok(mut s) = scrobble_state.lock() { | 1243 | if let Ok(mut s) = scrobble_state.lock() { |
| @@ -1291,7 +1291,7 @@ fn process_command( | |||
| 1291 | let new_pos = q.position() + 1; | 1291 | let new_pos = q.position() + 1; |
| 1292 | q.set_position(new_pos); | 1292 | q.set_position(new_pos); |
| 1293 | } | 1293 | } |
| 1294 | if let Some(track) = q.track_at(q.position()) { | 1294 | if let Some(track) = q.current() { |
| 1295 | audio.play(track.path.clone()); | 1295 | audio.play(track.path.clone()); |
| 1296 | spawn_send_now_playing(scrobbler, track); | 1296 | spawn_send_now_playing(scrobbler, track); |
| 1297 | if let Ok(mut s) = scrobble_state.lock() { | 1297 | if let Ok(mut s) = scrobble_state.lock() { |
| @@ -1389,7 +1389,7 @@ fn process_command( | |||
| 1389 | let new_pos = q.position() + 1; | 1389 | let new_pos = q.position() + 1; |
| 1390 | q.set_position(new_pos); | 1390 | q.set_position(new_pos); |
| 1391 | } | 1391 | } |
| 1392 | if let Some(track) = q.track_at(q.position()) { | 1392 | if let Some(track) = q.current() { |
| 1393 | audio.play(track.path.clone()); | 1393 | audio.play(track.path.clone()); |
| 1394 | spawn_send_now_playing(scrobbler, track); | 1394 | spawn_send_now_playing(scrobbler, track); |
| 1395 | if let Ok(mut s) = scrobble_state.lock() { | 1395 | if let Ok(mut s) = scrobble_state.lock() { |
diff --git a/src/queue.rs b/src/queue.rs index c3f8dbe..da79441 100644 --- a/src/queue.rs +++ b/src/queue.rs | |||
| @@ -5,7 +5,9 @@ | |||
| 5 | * https://codeberg.org/salmonik/RSPlayer | 5 | * https://codeberg.org/salmonik/RSPlayer |
| 6 | */ | 6 | */ |
| 7 | 7 | ||
| 8 | use rand::rngs::StdRng; | ||
| 8 | use rand::seq::SliceRandom; | 9 | use rand::seq::SliceRandom; |
| 10 | use rand::SeedableRng; | ||
| 9 | use std::path::{Path, PathBuf}; | 11 | use std::path::{Path, PathBuf}; |
| 10 | 12 | ||
| 11 | #[derive(Debug, Clone, PartialEq)] | 13 | #[derive(Debug, Clone, PartialEq)] |
| @@ -202,6 +204,7 @@ pub struct Queue { | |||
| 202 | position: usize, | 204 | position: usize, |
| 203 | shuffle: ShuffleMode, | 205 | shuffle: ShuffleMode, |
| 204 | shuffled_indices: Vec<usize>, | 206 | shuffled_indices: Vec<usize>, |
| 207 | rng: StdRng, | ||
| 205 | } | 208 | } |
| 206 | 209 | ||
| 207 | impl Default for Queue { | 210 | impl Default for Queue { |
| @@ -217,6 +220,20 @@ impl Queue { | |||
| 217 | position: 0, | 220 | position: 0, |
| 218 | shuffle: ShuffleMode::None, | 221 | shuffle: ShuffleMode::None, |
| 219 | shuffled_indices: Vec::new(), | 222 | shuffled_indices: Vec::new(), |
| 223 | rng: rand::make_rng(), | ||
| 224 | } | ||
| 225 | } | ||
| 226 | |||
| 227 | /// Construct a queue with a fixed RNG seed, making shuffle order fully | ||
| 228 | /// reproducible. Used by tests and handy for debugging a specific order. | ||
| 229 | #[allow(dead_code)] // used only by integration tests (bin crate does not link the lib) | ||
| 230 | pub fn with_seed(seed: u64) -> Self { | ||
| 231 | Queue { | ||
| 232 | tracks: Vec::new(), | ||
| 233 | position: 0, | ||
| 234 | shuffle: ShuffleMode::None, | ||
| 235 | shuffled_indices: Vec::new(), | ||
| 236 | rng: StdRng::seed_from_u64(seed), | ||
| 220 | } | 237 | } |
| 221 | } | 238 | } |
| 222 | 239 | ||
| @@ -225,7 +242,6 @@ impl Queue { | |||
| 225 | self.update_shuffle_indices(); | 242 | self.update_shuffle_indices(); |
| 226 | } | 243 | } |
| 227 | 244 | ||
| 228 | #[allow(dead_code)] | ||
| 229 | pub fn push_track(&mut self, track: Track) { | 245 | pub fn push_track(&mut self, track: Track) { |
| 230 | self.tracks.push(track); | 246 | self.tracks.push(track); |
| 231 | self.update_shuffle_indices(); | 247 | self.update_shuffle_indices(); |
| @@ -234,11 +250,27 @@ impl Queue { | |||
| 234 | pub fn insert_after_current(&mut self, path: PathBuf) { | 250 | pub fn insert_after_current(&mut self, path: PathBuf) { |
| 235 | if self.tracks.is_empty() { | 251 | if self.tracks.is_empty() { |
| 236 | self.tracks.push(Track::from_path(path)); | 252 | self.tracks.push(Track::from_path(path)); |
| 237 | } else { | 253 | self.update_shuffle_indices(); |
| 238 | let insert_idx = self.position + 1; | 254 | return; |
| 239 | self.tracks.insert(insert_idx, Track::from_path(path)); | ||
| 240 | } | 255 | } |
| 241 | self.update_shuffle_indices(); | 256 | let insert_idx = self.position + 1; |
| 257 | self.tracks.insert(insert_idx, Track::from_path(path)); | ||
| 258 | if self.shuffle == ShuffleMode::None { | ||
| 259 | self.update_shuffle_indices(); | ||
| 260 | return; | ||
| 261 | } | ||
| 262 | // Under shuffle, splice the new entry into the play order right after | ||
| 263 | // the current slot instead of re-shuffling the tail: the inserted | ||
| 264 | // track deterministically occupies slot `position + 1`, so "play | ||
| 265 | // now/next" is meaningful and `current()` agrees with what the daemon | ||
| 266 | // starts playing. The remaining play order is preserved (indices that | ||
| 267 | // shifted right in `tracks` are bumped), not re-randomized. | ||
| 268 | for v in self.shuffled_indices.iter_mut() { | ||
| 269 | if *v >= insert_idx { | ||
| 270 | *v += 1; | ||
| 271 | } | ||
| 272 | } | ||
| 273 | self.shuffled_indices.insert(self.position + 1, insert_idx); | ||
| 242 | } | 274 | } |
| 243 | 275 | ||
| 244 | pub fn track_at(&self, idx: usize) -> Option<&Track> { | 276 | pub fn track_at(&self, idx: usize) -> Option<&Track> { |
| @@ -309,10 +341,9 @@ impl Queue { | |||
| 309 | self.shuffled_indices = (0..len).collect(); | 341 | self.shuffled_indices = (0..len).collect(); |
| 310 | match self.shuffle { | 342 | match self.shuffle { |
| 311 | ShuffleMode::All => { | 343 | ShuffleMode::All => { |
| 312 | let mut rng = rand::rng(); | ||
| 313 | let pos = self.position.min(len.saturating_sub(1)); | 344 | let pos = self.position.min(len.saturating_sub(1)); |
| 314 | if pos + 1 < len { | 345 | if pos + 1 < len { |
| 315 | self.shuffled_indices[pos + 1..].shuffle(&mut rng); | 346 | self.shuffled_indices[pos + 1..].shuffle(&mut self.rng); |
| 316 | } | 347 | } |
| 317 | } | 348 | } |
| 318 | ShuffleMode::Album => { | 349 | ShuffleMode::Album => { |
| @@ -330,10 +361,14 @@ impl Queue { | |||
| 330 | self.shuffled_indices = (0..len).collect(); | 361 | self.shuffled_indices = (0..len).collect(); |
| 331 | 362 | ||
| 332 | if len > 1 { | 363 | if len > 1 { |
| 333 | let mut rng = rand::rng(); | ||
| 334 | let pos = self.position.min(len.saturating_sub(1)); | 364 | let pos = self.position.min(len.saturating_sub(1)); |
| 335 | if pos + 1 < len { | 365 | if pos + 1 < len { |
| 336 | self.shuffle_album_range(pos + 1..len, &mut rng); | 366 | Self::shuffle_album_range( |
| 367 | &self.tracks, | ||
| 368 | &mut self.shuffled_indices, | ||
| 369 | pos + 1..len, | ||
| 370 | &mut self.rng, | ||
| 371 | ); | ||
| 337 | } | 372 | } |
| 338 | } | 373 | } |
| 339 | } | 374 | } |
| @@ -352,7 +387,6 @@ impl Queue { | |||
| 352 | } | 387 | } |
| 353 | 388 | ||
| 354 | let range_start = pos + 1; | 389 | let range_start = pos + 1; |
| 355 | let mut rng = rand::rng(); | ||
| 356 | 390 | ||
| 357 | // Build album groups within the range, shuffling tracks within each group | 391 | // Build album groups within the range, shuffling tracks within each group |
| 358 | let mut groups: Vec<Vec<usize>> = Vec::new(); | 392 | let mut groups: Vec<Vec<usize>> = Vec::new(); |
| @@ -368,7 +402,7 @@ impl Queue { | |||
| 368 | } else if album != current_album { | 402 | } else if album != current_album { |
| 369 | // Shuffle tracks within this album group before adding it | 403 | // Shuffle tracks within this album group before adding it |
| 370 | if current_group.len() > 1 { | 404 | if current_group.len() > 1 { |
| 371 | current_group.shuffle(&mut rng); | 405 | current_group.shuffle(&mut self.rng); |
| 372 | } | 406 | } |
| 373 | groups.push(std::mem::take(&mut current_group)); | 407 | groups.push(std::mem::take(&mut current_group)); |
| 374 | current_group.push(i); | 408 | current_group.push(i); |
| @@ -379,13 +413,13 @@ impl Queue { | |||
| 379 | } | 413 | } |
| 380 | if !current_group.is_empty() { | 414 | if !current_group.is_empty() { |
| 381 | if current_group.len() > 1 { | 415 | if current_group.len() > 1 { |
| 382 | current_group.shuffle(&mut rng); | 416 | current_group.shuffle(&mut self.rng); |
| 383 | } | 417 | } |
| 384 | groups.push(current_group); | 418 | groups.push(current_group); |
| 385 | } | 419 | } |
| 386 | 420 | ||
| 387 | // Shuffle the album group order | 421 | // Shuffle the album group order |
| 388 | groups.shuffle(&mut rng); | 422 | groups.shuffle(&mut self.rng); |
| 389 | 423 | ||
| 390 | let mut new_indices: Vec<usize> = self.shuffled_indices[..=pos].to_vec(); | 424 | let mut new_indices: Vec<usize> = self.shuffled_indices[..=pos].to_vec(); |
| 391 | for group in &groups { | 425 | for group in &groups { |
| @@ -394,7 +428,12 @@ impl Queue { | |||
| 394 | self.shuffled_indices = new_indices; | 428 | self.shuffled_indices = new_indices; |
| 395 | } | 429 | } |
| 396 | 430 | ||
| 397 | fn shuffle_album_range(&mut self, range: std::ops::Range<usize>, rng: &mut impl rand::Rng) { | 431 | fn shuffle_album_range( |
| 432 | tracks: &[Track], | ||
| 433 | indices: &mut [usize], | ||
| 434 | range: std::ops::Range<usize>, | ||
| 435 | rng: &mut impl rand::Rng, | ||
| 436 | ) { | ||
| 398 | if range.start >= range.end { | 437 | if range.start >= range.end { |
| 399 | return; | 438 | return; |
| 400 | } | 439 | } |
| @@ -404,7 +443,7 @@ impl Queue { | |||
| 404 | let mut range_start: Option<usize> = None; | 443 | let mut range_start: Option<usize> = None; |
| 405 | 444 | ||
| 406 | for i in range.clone() { | 445 | for i in range.clone() { |
| 407 | let album = self.tracks.get(i).and_then(|t| t.album.as_deref()); | 446 | let album = tracks.get(i).and_then(|t| t.album.as_deref()); |
| 408 | match (current_album, range_start) { | 447 | match (current_album, range_start) { |
| 409 | (None, None) => { | 448 | (None, None) => { |
| 410 | current_album = album; | 449 | current_album = album; |
| @@ -424,7 +463,7 @@ impl Queue { | |||
| 424 | 463 | ||
| 425 | for (start, end) in album_ranges { | 464 | for (start, end) in album_ranges { |
| 426 | if start + 1 < end { | 465 | if start + 1 < end { |
| 427 | self.shuffled_indices[start..end].shuffle(rng); | 466 | indices[start..end].shuffle(rng); |
| 428 | } | 467 | } |
| 429 | } | 468 | } |
| 430 | } | 469 | } |
| @@ -498,13 +537,17 @@ impl Queue { | |||
| 498 | if self.position != 0 || self.tracks.len() <= 1 { | 537 | if self.position != 0 || self.tracks.len() <= 1 { |
| 499 | return; | 538 | return; |
| 500 | } | 539 | } |
| 501 | let mut rng = rand::rng(); | ||
| 502 | match self.shuffle { | 540 | match self.shuffle { |
| 503 | ShuffleMode::All => { | 541 | ShuffleMode::All => { |
| 504 | self.shuffled_indices.shuffle(&mut rng); | 542 | self.shuffled_indices.shuffle(&mut self.rng); |
| 505 | } | 543 | } |
| 506 | ShuffleMode::Album => { | 544 | ShuffleMode::Album => { |
| 507 | self.shuffle_album_range(0..self.tracks.len(), &mut rng); | 545 | Self::shuffle_album_range( |
| 546 | &self.tracks, | ||
| 547 | &mut self.shuffled_indices, | ||
| 548 | 0..self.tracks.len(), | ||
| 549 | &mut self.rng, | ||
| 550 | ); | ||
| 508 | } | 551 | } |
| 509 | ShuffleMode::Albums => { | 552 | ShuffleMode::Albums => { |
| 510 | let mut groups: Vec<Vec<usize>> = Vec::new(); | 553 | let mut groups: Vec<Vec<usize>> = Vec::new(); |
| @@ -519,7 +562,7 @@ impl Queue { | |||
| 519 | current_album = album; | 562 | current_album = album; |
| 520 | } else if album != current_album { | 563 | } else if album != current_album { |
| 521 | if current_group.len() > 1 { | 564 | if current_group.len() > 1 { |
| 522 | current_group.shuffle(&mut rng); | 565 | current_group.shuffle(&mut self.rng); |
| 523 | } | 566 | } |
| 524 | groups.push(std::mem::take(&mut current_group)); | 567 | groups.push(std::mem::take(&mut current_group)); |
| 525 | current_group.push(i); | 568 | current_group.push(i); |
| @@ -530,12 +573,12 @@ impl Queue { | |||
| 530 | } | 573 | } |
| 531 | if !current_group.is_empty() { | 574 | if !current_group.is_empty() { |
| 532 | if current_group.len() > 1 { | 575 | if current_group.len() > 1 { |
| 533 | current_group.shuffle(&mut rng); | 576 | current_group.shuffle(&mut self.rng); |
| 534 | } | 577 | } |
| 535 | groups.push(current_group); | 578 | groups.push(current_group); |
| 536 | } | 579 | } |
| 537 | 580 | ||
| 538 | groups.shuffle(&mut rng); | 581 | groups.shuffle(&mut self.rng); |
| 539 | 582 | ||
| 540 | self.shuffled_indices = groups.concat(); | 583 | self.shuffled_indices = groups.concat(); |
| 541 | } | 584 | } |
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() { | |||
| 567 | assert_eq!(q.position(), 1); | 567 | assert_eq!(q.position(), 1); |
| 568 | } | 568 | } |
| 569 | 569 | ||
| 570 | #[test] | ||
| 571 | fn test_shuffle_play_now_current_is_inserted_track() { | ||
| 572 | // BUG: under shuffle, `insert_after_current` re-shuffles the tail | ||
| 573 | // (`update_shuffle_indices`), so the inserted track's play-order slot is a | ||
| 574 | // random draw instead of `position + 1`. The daemon's "now" flow (insert → | ||
| 575 | // `set_position(pos+1)` → play `track_at(position)`, src/daemon.rs) then | ||
| 576 | // starts the right file while `current()`/`next()`/the queue display | ||
| 577 | // follow a *different* track, and auto-advance can replay or skip the | ||
| 578 | // inserted track entirely. | ||
| 579 | // | ||
| 580 | // Contract: after the "now" sequence, `current()` must be the inserted | ||
| 581 | // track. The seed sweep keeps the test deterministic — for a fixed seed the | ||
| 582 | // outcome never varies between runs, and pre-fix a given seed satisfies the | ||
| 583 | // invariant with probability 1/(tail size) = 1/3, so the chance all 40 | ||
| 584 | // seeds accidentally pass is ~3e-19. The fix makes the invariant hold | ||
| 585 | // structurally (insert into the play order at `position+1`), independent | ||
| 586 | // of the seed. | ||
| 587 | let inserted = PathBuf::from("/music/new.mp3"); | ||
| 588 | for seed in 1..=40u64 { | ||
| 589 | let mut q = Queue::with_seed(seed); | ||
| 590 | add_track(&mut q, "/music/a.mp3"); | ||
| 591 | add_track(&mut q, "/music/b.mp3"); | ||
| 592 | add_track(&mut q, "/music/c.mp3"); | ||
| 593 | q.set_shuffle(ShuffleMode::All); | ||
| 594 | |||
| 595 | // `rsp play --now new.mp3` with a non-empty queue: insert after the | ||
| 596 | // current track and move the cursor onto it. | ||
| 597 | q.insert_after_current(inserted.clone()); | ||
| 598 | let new_pos = q.position() + 1; | ||
| 599 | q.set_position(new_pos); | ||
| 600 | |||
| 601 | assert_eq!(q.position(), 1); | ||
| 602 | let cur = q.current().expect("queue is not empty"); | ||
| 603 | assert_eq!( | ||
| 604 | cur.path, inserted, | ||
| 605 | "seed {}: current() after play-now is {:?}, expected {:?}; shuffled_indices={:?}", | ||
| 606 | seed, | ||
| 607 | cur.path, | ||
| 608 | inserted, | ||
| 609 | q.shuffled_indices() | ||
| 610 | ); | ||
| 611 | } | ||
| 612 | } | ||
| 613 | |||
| 614 | // ---------- Seedable RNG ---------- | ||
| 615 | // The queue uses a seeded PRNG (rand::rngs::StdRng) instead of the OS-seeded | ||
| 616 | // thread RNG, so shuffle order is reproducible via Queue::with_seed. Shuffle | ||
| 617 | // is not a security domain — a PRNG suffices, and deterministic tests need a | ||
| 618 | // fixed seed. | ||
| 619 | |||
| 620 | #[test] | ||
| 621 | fn test_same_seed_produces_same_shuffle_order() { | ||
| 622 | let mut q1 = Queue::with_seed(0x5EED); | ||
| 623 | let mut q2 = Queue::with_seed(0x5EED); | ||
| 624 | for i in 0..10 { | ||
| 625 | add_track(&mut q1, &format!("/music/t{}.mp3", i)); | ||
| 626 | add_track(&mut q2, &format!("/music/t{}.mp3", i)); | ||
| 627 | } | ||
| 628 | q1.set_shuffle(ShuffleMode::All); | ||
| 629 | q2.set_shuffle(ShuffleMode::All); | ||
| 630 | assert_eq!(q1.shuffled_indices(), q2.shuffled_indices()); | ||
| 631 | } | ||
| 632 | |||
| 633 | #[test] | ||
| 634 | fn test_different_seed_produces_different_shuffle_order() { | ||
| 635 | let mut q1 = Queue::with_seed(1); | ||
| 636 | let mut q2 = Queue::with_seed(2); | ||
| 637 | for i in 0..20 { | ||
| 638 | add_track(&mut q1, &format!("/music/t{}.mp3", i)); | ||
| 639 | add_track(&mut q2, &format!("/music/t{}.mp3", i)); | ||
| 640 | } | ||
| 641 | q1.set_shuffle(ShuffleMode::All); | ||
| 642 | q2.set_shuffle(ShuffleMode::All); | ||
| 643 | // 20! permutations — identical order for two seeds is practically impossible. | ||
| 644 | assert_ne!(q1.shuffled_indices(), q2.shuffled_indices()); | ||
| 645 | } | ||
| 646 | |||
| 570 | // ---------- Track struct ---------- | 647 | // ---------- Track struct ---------- |
| 571 | 648 | ||
| 572 | #[test] | 649 | #[test] |
