(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:
dvdsk 2024-04-04 16:26:42 +02:00
parent 26e9db7b87
commit 7eb13be288
No known key found for this signature in database
GPG key ID: 6CF9D20C5709A836
6 changed files with 170 additions and 153 deletions

View file

@ -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

Binary file not shown.

BIN
assets/RL.m4a Normal file

Binary file not shown.

BIN
assets/RL.mp3 Normal file

Binary file not shown.

BIN
assets/RL.wav Normal file

Binary file not shown.

View file

@ -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
}