mirror of
https://github.com/RustAudio/rodio
synced 2024-11-10 06:04:16 +00:00
(tests) use rstest to refactor and expand seek test
This adds two dependencies, they are only needed for the test suite. Compile time will not increase for users. The extra overhead when running the test suite is worth it imho. The test file is significantly shorter and there is less code duplication. A run time solution would decrease the test interface (you would have to manually find out which params caused the test).
This commit is contained in:
parent
26e9db7b87
commit
7eb13be288
6 changed files with 170 additions and 153 deletions
|
@ -38,6 +38,8 @@ symphonia-wav = ["symphonia/wav", "symphonia/pcm", "symphonia/adpcm"]
|
|||
|
||||
[dev-dependencies]
|
||||
quickcheck = "0.9.2"
|
||||
rstest = "0.18.2"
|
||||
rstest_reuse = "0.6.0"
|
||||
|
||||
[[example]]
|
||||
name = "music_m4a"
|
||||
|
|
BIN
assets/RL.flac
Normal file
BIN
assets/RL.flac
Normal file
Binary file not shown.
BIN
assets/RL.m4a
Normal file
BIN
assets/RL.m4a
Normal file
Binary file not shown.
BIN
assets/RL.mp3
Normal file
BIN
assets/RL.mp3
Normal file
Binary file not shown.
BIN
assets/RL.wav
Normal file
BIN
assets/RL.wav
Normal file
Binary file not shown.
321
tests/seek.rs
321
tests/seek.rs
|
@ -3,104 +3,169 @@ use std::path::Path;
|
|||
use std::time::Duration;
|
||||
|
||||
use rodio::{Decoder, Source};
|
||||
use rstest::rstest;
|
||||
use rstest_reuse::{self, *};
|
||||
|
||||
fn time_remaining(decoder: Decoder<impl Read + Seek>) -> Duration {
|
||||
let rate = decoder.sample_rate() as f64;
|
||||
let n_channels = decoder.channels() as f64;
|
||||
let n_samples = decoder.into_iter().count() as f64;
|
||||
dbg!(n_samples);
|
||||
Duration::from_secs_f64(n_samples / rate / n_channels)
|
||||
#[template]
|
||||
#[rstest]
|
||||
// note: disabled, broken decoder see issue: #516
|
||||
// #[cfg_attr(feature = "symphonia-vorbis"), case("ogg", true, "symphonia")],
|
||||
#[cfg_attr(
|
||||
all(feature = "minimp3", not(feature = "symphonia-mp3")),
|
||||
case("mp3", false, "minimp3")
|
||||
)]
|
||||
#[cfg_attr(
|
||||
all(feature = "wav", not(feature = "symphonia-wav")),
|
||||
case("wav", true, "hound")
|
||||
)]
|
||||
#[cfg_attr(
|
||||
all(feature = "flac", not(feature = "symphonia-flac")),
|
||||
case("flac", false, "claxon")
|
||||
)]
|
||||
#[cfg_attr(feature = "symphonia-mp3", case("mp3", true, "symphonia"))]
|
||||
#[cfg_attr(feature = "symponia-isomp4", case("m4a", true, "symphonia"))]
|
||||
#[cfg_attr(feature = "symphonia-wav", case("wav", true, "symphonia"))]
|
||||
#[cfg_attr(feature = "symphonia-flac", case("flac", true, "symphonia"))]
|
||||
fn all_decoders(
|
||||
#[case] format: &'static str,
|
||||
#[case] supports_seek: bool,
|
||||
#[case] decoder_name: &'static str,
|
||||
) {
|
||||
}
|
||||
|
||||
fn get_decoder(format: &str) -> Decoder<impl Read + Seek> {
|
||||
let asset = Path::new("assets/music").with_extension(format);
|
||||
let file = std::fs::File::open(asset).unwrap();
|
||||
let decoder = rodio::Decoder::new(BufReader::new(file)).unwrap();
|
||||
decoder
|
||||
#[template]
|
||||
#[rstest]
|
||||
// note: disabled, broken decoder see issue: #516
|
||||
// #[cfg_attr(feature = "symphonia-vorbis"), case("ogg", true, "symphonia")],
|
||||
#[cfg_attr(
|
||||
all(feature = "wav", not(feature = "symphonia-wav")),
|
||||
case("wav", "hound")
|
||||
)]
|
||||
#[cfg_attr(feature = "symphonia-mp3", case("mp3", "symphonia"))]
|
||||
#[cfg_attr(feature = "symponia-isomp4", case("m4a", "symphonia"))]
|
||||
#[cfg_attr(feature = "symphonia-wav", case("wav", "symphonia"))]
|
||||
#[cfg_attr(feature = "symphonia-flac", case("flac", "symphonia"))]
|
||||
fn supported_decoders(#[case] format: &'static str, #[case] decoder_name: &'static str) {}
|
||||
|
||||
#[apply(all_decoders)]
|
||||
#[trace]
|
||||
fn seek_returns_err_if_unsupported(
|
||||
#[case] format: &'static str,
|
||||
#[case] supports_seek: bool,
|
||||
#[case] decoder_name: &'static str,
|
||||
) {
|
||||
let mut decoder = get_music(format);
|
||||
let res = decoder.try_seek(Duration::from_millis(2500));
|
||||
assert_eq!(res.is_ok(), supports_seek, "decoder: {decoder_name}");
|
||||
}
|
||||
|
||||
// run tests twice to test all decoders
|
||||
// cargo test
|
||||
// cargo test --features symphonia-all
|
||||
fn format_decoder_info() -> &'static [(&'static str, bool, &'static str)] {
|
||||
&[
|
||||
#[cfg(all(feature = "minimp3", not(feature = "symphonia-mp3")))]
|
||||
("mp3", false, "minimp3"),
|
||||
#[cfg(feature = "symphonia-mp3")]
|
||||
("mp3", true, "symphonia"),
|
||||
#[cfg(all(feature = "wav", not(feature = "symphonia-wav")))]
|
||||
("wav", true, "hound"),
|
||||
#[cfg(feature = "symphonia-wav")]
|
||||
("wav", true, "symphonia"),
|
||||
#[cfg(all(feature = "vorbis", not(feature = "symphonia-vorbis")))]
|
||||
("ogg", true, "lewton"),
|
||||
// note: disabled, broken decoder see issue: #516
|
||||
// #[cfg(feature = "symphonia-vorbis")]
|
||||
// ("ogg", true, "symphonia"),
|
||||
#[cfg(all(feature = "flac", not(feature = "symphonia-flac")))]
|
||||
("flac", false, "claxon"),
|
||||
#[cfg(feature = "symphonia-flac")]
|
||||
("flac", true, "symphonia"),
|
||||
// note: disabled, symphonia returns error unsupported format
|
||||
#[cfg(feature = "symphonia-isomp4")]
|
||||
("m4a", true, "symphonia"),
|
||||
]
|
||||
#[apply(supported_decoders)]
|
||||
#[trace]
|
||||
fn seek_beyond_end_saturates(#[case] format: &'static str, #[case] decoder_name: &'static str) {
|
||||
let mut decoder = get_music(format);
|
||||
println!("seeking beyond end for: {format}\t decoded by: {decoder_name}");
|
||||
let res = decoder.try_seek(Duration::from_secs(999));
|
||||
|
||||
assert!(res.is_ok());
|
||||
assert!(time_remaining(decoder) < Duration::from_secs(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seek_returns_err_if_unsupported() {
|
||||
for (format, supported, decoder_name) in format_decoder_info().iter().cloned() {
|
||||
println!("trying: {format},\t\tby: {decoder_name},\t\tshould support seek: {supported}");
|
||||
let mut decoder = get_decoder(format);
|
||||
let res = decoder.try_seek(Duration::from_millis(2500));
|
||||
assert_eq!(res.is_ok(), supported, "decoder: {decoder_name}");
|
||||
}
|
||||
}
|
||||
#[apply(supported_decoders)]
|
||||
#[trace]
|
||||
fn seek_results_in_correct_remaining_playtime(
|
||||
#[case] format: &'static str,
|
||||
#[case] decoder_name: &'static str,
|
||||
) {
|
||||
println!("checking seek duration for: {format}\t decoded by: {decoder_name}");
|
||||
|
||||
#[test] // in the future use PR #510 (playback position) to speed this up
|
||||
fn seek_beyond_end_saturates() {
|
||||
for (format, _, decoder_name) in format_decoder_info()
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|(_, supported, _)| *supported)
|
||||
{
|
||||
let mut decoder = get_decoder(format);
|
||||
let decoder = get_music(format);
|
||||
let total_duration = time_remaining(decoder);
|
||||
dbg!(total_duration);
|
||||
|
||||
println!("seeking beyond end for: {format}\t decoded by: {decoder_name}");
|
||||
let res = decoder.try_seek(Duration::from_secs(999));
|
||||
assert!(res.is_ok());
|
||||
const SEEK_BEFORE_END: Duration = Duration::from_secs(5);
|
||||
let mut decoder = get_music(format);
|
||||
decoder.try_seek(total_duration - SEEK_BEFORE_END).unwrap();
|
||||
|
||||
assert!(time_remaining(decoder) < Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
let after_seek = time_remaining(decoder);
|
||||
let expected = SEEK_BEFORE_END;
|
||||
|
||||
#[test]
|
||||
fn seek_results_in_correct_remaining_playtime() {
|
||||
for (format, _, decoder_name) in format_decoder_info()
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter(|(_, supported, _)| *supported)
|
||||
{
|
||||
println!("checking seek duration for: {format}\t decoded by: {decoder_name}");
|
||||
|
||||
let decoder = get_decoder(format);
|
||||
let total_duration = time_remaining(decoder);
|
||||
dbg!(total_duration);
|
||||
|
||||
const SEEK_BEFORE_END: Duration = Duration::from_secs(5);
|
||||
let mut decoder = get_decoder(format);
|
||||
decoder.try_seek(total_duration - SEEK_BEFORE_END).unwrap();
|
||||
|
||||
let after_seek = time_remaining(decoder);
|
||||
let expected = SEEK_BEFORE_END;
|
||||
|
||||
if after_seek.as_millis().abs_diff(expected.as_millis()) > 250 {
|
||||
panic!(
|
||||
"Seek did not result in expected leftover playtime
|
||||
if after_seek.as_millis().abs_diff(expected.as_millis()) > 250 {
|
||||
panic!(
|
||||
"Seek did not result in expected leftover playtime
|
||||
leftover time: {after_seek:?}
|
||||
expected time left in source: {SEEK_BEFORE_END:?}"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[apply(supported_decoders)]
|
||||
#[trace]
|
||||
fn seek_possible_after_exausting_source(
|
||||
#[case] format: &'static str,
|
||||
#[case] _decoder_name: &'static str,
|
||||
) {
|
||||
let mut source = get_music(format);
|
||||
while source.next().is_some() {}
|
||||
assert!(source.next().is_none());
|
||||
|
||||
source.try_seek(Duration::from_secs(0)).unwrap();
|
||||
assert!(source.next().is_some());
|
||||
}
|
||||
|
||||
#[apply(supported_decoders)]
|
||||
#[trace]
|
||||
fn seek_does_not_break_channel_order(
|
||||
#[case] format: &'static str,
|
||||
#[case] _decoder_name: &'static str,
|
||||
) {
|
||||
let mut source = get_rl(format).convert_samples();
|
||||
let channels = source.channels();
|
||||
assert_eq!(channels, 2, "test needs a stereo beep file");
|
||||
|
||||
let beep_range = second_channel_beep_range(&mut source);
|
||||
let beep_start = Duration::from_secs_f32(
|
||||
beep_range.start as f32 / source.channels() as f32 / source.sample_rate() as f32,
|
||||
);
|
||||
|
||||
let mut source = get_rl(format).convert_samples();
|
||||
|
||||
const WINDOW: usize = 100;
|
||||
let samples: Vec<_> = source
|
||||
.by_ref()
|
||||
.skip(beep_range.start)
|
||||
.take(WINDOW)
|
||||
.collect();
|
||||
assert!(is_silent(&samples, channels, 0), "{samples:?}");
|
||||
assert!(!is_silent(&samples, channels, 1), "{samples:?}");
|
||||
|
||||
let mut channel_offset = 0;
|
||||
for offset in [1, 4, 7, 40, 41, 120, 179]
|
||||
.map(|offset| offset as f32 / (source.sample_rate() as f32))
|
||||
.map(Duration::from_secs_f32)
|
||||
{
|
||||
source.next(); // WINDOW is even, make the amount of calls to next
|
||||
// uneven to force issues with channels alternating
|
||||
// between seek to surface
|
||||
channel_offset = (channel_offset + 1) % 2;
|
||||
|
||||
source.try_seek(beep_start + offset).unwrap();
|
||||
let samples: Vec<_> = source.by_ref().take(WINDOW).collect();
|
||||
let channel0 = 0 + channel_offset;
|
||||
assert!(
|
||||
is_silent(&samples, source.channels(), channel0),
|
||||
"channel0 should be silent,
|
||||
channel0 starts at idx: {channel0}
|
||||
seek: {beep_start:?} + {offset:?}
|
||||
samples: {samples:?}"
|
||||
);
|
||||
let channel1 = (1 + channel_offset) % 2;
|
||||
assert!(
|
||||
!is_silent(&samples, source.channels(), channel1),
|
||||
"channel1 should not be silent,
|
||||
channel1; starts at idx: {channel1}
|
||||
seek: {beep_start:?} + {offset:?}
|
||||
samples: {samples:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -158,74 +223,24 @@ fn is_silent(samples: &[f32], channels: u16, channel: usize) -> bool {
|
|||
volume < BASICALLY_ZERO
|
||||
}
|
||||
|
||||
// TODO test all decoders
|
||||
#[test]
|
||||
fn seek_does_not_break_channel_order() {
|
||||
let file = std::fs::File::open("assets/RL.ogg").unwrap();
|
||||
let mut source = rodio::Decoder::new(BufReader::new(file))
|
||||
.unwrap()
|
||||
.convert_samples();
|
||||
let channels = source.channels();
|
||||
assert_eq!(channels, 2, "test needs a stereo beep file");
|
||||
|
||||
let beep_range = second_channel_beep_range(&mut source);
|
||||
let beep_start = Duration::from_secs_f32(
|
||||
beep_range.start as f32 / source.channels() as f32 / source.sample_rate() as f32,
|
||||
);
|
||||
|
||||
let file = std::fs::File::open("assets/RL.ogg").unwrap();
|
||||
let mut source = rodio::Decoder::new(BufReader::new(file))
|
||||
.unwrap()
|
||||
.convert_samples();
|
||||
|
||||
const WINDOW: usize = 100;
|
||||
let samples: Vec<_> = source
|
||||
.by_ref()
|
||||
.skip(beep_range.start)
|
||||
.take(WINDOW)
|
||||
.collect();
|
||||
assert!(is_silent(&samples, channels, 0), "{samples:?}");
|
||||
assert!(!is_silent(&samples, channels, 1), "{samples:?}");
|
||||
|
||||
let mut channel_offset = 0;
|
||||
for offset in [1, 4, 7, 40, 41, 120, 179]
|
||||
.map(|offset| offset as f32 / (source.sample_rate() as f32))
|
||||
.map(Duration::from_secs_f32)
|
||||
{
|
||||
source.next(); // WINDOW is even, make the amount of calls to next
|
||||
// uneven to force issues with channels alternating
|
||||
// between seek to surface
|
||||
channel_offset = (channel_offset + 1) % 2;
|
||||
|
||||
source.try_seek(beep_start + offset).unwrap();
|
||||
let samples: Vec<_> = source.by_ref().take(WINDOW).collect();
|
||||
let channel0 = 0 + channel_offset;
|
||||
assert!(
|
||||
is_silent(&samples, source.channels(), channel0),
|
||||
"channel0 should be silent,
|
||||
channel0 starts at idx: {channel0}
|
||||
seek: {beep_start:?} + {offset:?}
|
||||
samples: {samples:?}"
|
||||
);
|
||||
let channel1 = (1 + channel_offset) % 2;
|
||||
assert!(
|
||||
!is_silent(&samples, source.channels(), channel1),
|
||||
"channel1 should not be silent,
|
||||
channel1; starts at idx: {channel1}
|
||||
seek: {beep_start:?} + {offset:?}
|
||||
samples: {samples:?}"
|
||||
);
|
||||
}
|
||||
fn time_remaining(decoder: Decoder<impl Read + Seek>) -> Duration {
|
||||
let rate = decoder.sample_rate() as f64;
|
||||
let n_channels = decoder.channels() as f64;
|
||||
let n_samples = decoder.into_iter().count() as f64;
|
||||
Duration::from_secs_f64(n_samples / rate / n_channels)
|
||||
}
|
||||
|
||||
// TODO test all decoders
|
||||
#[test]
|
||||
fn seek_possible_after_exausting_source() {
|
||||
let file = std::fs::File::open("assets/RL.ogg").unwrap();
|
||||
let mut source = rodio::Decoder::new(BufReader::new(file)).unwrap();
|
||||
while source.next().is_some() {}
|
||||
assert!(source.next().is_none());
|
||||
|
||||
source.try_seek(Duration::from_secs(0)).unwrap();
|
||||
assert!(source.next().is_some());
|
||||
fn get_music(format: &str) -> Decoder<impl Read + Seek> {
|
||||
let asset = Path::new("assets/music").with_extension(format);
|
||||
let file = std::fs::File::open(asset).unwrap();
|
||||
let decoder = rodio::Decoder::new(BufReader::new(file)).unwrap();
|
||||
decoder
|
||||
}
|
||||
|
||||
fn get_rl(format: &str) -> Decoder<impl Read + Seek> {
|
||||
let asset = Path::new("assets/RL").with_extension(format);
|
||||
println!("opening: {}", asset.display());
|
||||
let file = std::fs::File::open(asset).unwrap();
|
||||
let decoder = rodio::Decoder::new(BufReader::new(file)).unwrap();
|
||||
decoder
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue