Skip to content

Commit 4e8bb5c

Browse files
committed
[add] more tests.
1 parent 17827d6 commit 4e8bb5c

File tree

1 file changed

+190
-2
lines changed

1 file changed

+190
-2
lines changed

crates/lambda-rs/src/audio/playback/context.rs

Lines changed: 190 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ impl SoundInstance {
138138

139139
/// A playback context owning an output device and one active playback slot.
140140
pub struct AudioContext {
141-
_output_device: AudioOutputDevice,
141+
_output_device: Option<AudioOutputDevice>,
142142
command_queue: Arc<PlaybackCommandQueue>,
143143
shared_state: Arc<PlaybackSharedState>,
144144
next_instance_id: u64,
@@ -258,7 +258,7 @@ impl AudioContextBuilder {
258258
)?;
259259

260260
return Ok(AudioContext {
261-
_output_device: output_device,
261+
_output_device: Some(output_device),
262262
command_queue,
263263
shared_state,
264264
next_instance_id: 1,
@@ -378,6 +378,37 @@ impl AudioContext {
378378
mod tests {
379379
use super::*;
380380

381+
fn create_test_context(sample_rate: u32, channels: u16) -> AudioContext {
382+
return AudioContext {
383+
_output_device: None,
384+
command_queue: Arc::new(CommandQueue::new()),
385+
shared_state: Arc::new(PlaybackSharedState::new()),
386+
next_instance_id: 1,
387+
output_sample_rate: sample_rate,
388+
output_channels: channels,
389+
};
390+
}
391+
392+
fn create_test_sound_buffer(
393+
sample_rate: u32,
394+
channels: u16,
395+
frames: usize,
396+
) -> SoundBuffer {
397+
let sample_count = frames * channels as usize;
398+
let samples = vec![0.0; sample_count];
399+
return SoundBuffer::from_interleaved_samples_for_test(
400+
samples,
401+
sample_rate,
402+
channels,
403+
)
404+
.expect("test sound buffer must be valid");
405+
}
406+
407+
fn fill_command_queue(queue: &PlaybackCommandQueue) {
408+
while queue.push(PlaybackCommand::StopCurrent).is_ok() {}
409+
return;
410+
}
411+
381412
/// `SoundInstance` methods MUST be no-ops when the instance is inactive.
382413
#[test]
383414
fn sound_instance_is_no_op_when_inactive() {
@@ -523,4 +554,161 @@ mod tests {
523554
assert_eq!(builder.label.as_deref(), Some("test-context"));
524555
return;
525556
}
557+
558+
/// The builder MUST reject invalid sample rates before device selection.
559+
#[test]
560+
fn audio_context_builder_rejects_invalid_sample_rate() {
561+
let result = AudioContextBuilder::new().with_sample_rate(0).build();
562+
assert!(matches!(
563+
result,
564+
Err(AudioError::InvalidSampleRate { requested: 0 })
565+
));
566+
return;
567+
}
568+
569+
/// The builder MUST reject invalid channel counts before device selection.
570+
#[test]
571+
fn audio_context_builder_rejects_invalid_channels() {
572+
let result = AudioContextBuilder::new().with_channels(0).build();
573+
assert!(matches!(
574+
result,
575+
Err(AudioError::InvalidChannels { requested: 0 })
576+
));
577+
return;
578+
}
579+
580+
/// `play_sound` MUST reject sound buffers with mismatched sample rates.
581+
#[test]
582+
fn play_sound_rejects_sample_rate_mismatch() {
583+
let mut context = create_test_context(48_000, 2);
584+
let buffer = create_test_sound_buffer(44_100, 2, 4);
585+
586+
let result = context.play_sound(&buffer);
587+
assert!(matches!(result, Err(AudioError::InvalidData { .. })));
588+
589+
assert!(context.command_queue.pop().is_none());
590+
assert_eq!(context.shared_state.active_instance_id(), 0);
591+
assert_eq!(context.shared_state.state(), PlaybackState::Stopped);
592+
return;
593+
}
594+
595+
/// `play_sound` MUST reject sound buffers with mismatched channel counts.
596+
#[test]
597+
fn play_sound_rejects_channel_mismatch() {
598+
let mut context = create_test_context(48_000, 2);
599+
let buffer = create_test_sound_buffer(48_000, 1, 4);
600+
601+
let result = context.play_sound(&buffer);
602+
assert!(matches!(result, Err(AudioError::InvalidData { .. })));
603+
604+
assert!(context.command_queue.pop().is_none());
605+
assert_eq!(context.shared_state.active_instance_id(), 0);
606+
assert_eq!(context.shared_state.state(), PlaybackState::Stopped);
607+
return;
608+
}
609+
610+
/// `play_sound` MUST reject empty sound buffers.
611+
#[test]
612+
fn play_sound_rejects_empty_samples() {
613+
let mut context = create_test_context(48_000, 2);
614+
let buffer = create_test_sound_buffer(48_000, 2, 0 /* frames */);
615+
616+
let result = context.play_sound(&buffer);
617+
assert!(matches!(result, Err(AudioError::InvalidData { .. })));
618+
619+
assert!(context.command_queue.pop().is_none());
620+
assert_eq!(context.shared_state.active_instance_id(), 0);
621+
assert_eq!(context.shared_state.state(), PlaybackState::Stopped);
622+
return;
623+
}
624+
625+
/// `play_sound` MUST schedule stop, buffer, then play commands.
626+
#[test]
627+
fn play_sound_enqueues_commands_and_updates_state() {
628+
let mut context = create_test_context(48_000, 2);
629+
let buffer = create_test_sound_buffer(48_000, 2, 4);
630+
631+
let instance = context.play_sound(&buffer).expect("must play sound");
632+
assert_eq!(instance.instance_id, 1);
633+
assert_eq!(context.shared_state.active_instance_id(), 1);
634+
assert_eq!(context.shared_state.state(), PlaybackState::Playing);
635+
assert_eq!(instance.state(), PlaybackState::Playing);
636+
637+
assert!(matches!(
638+
context.command_queue.pop(),
639+
Some(PlaybackCommand::StopCurrent)
640+
));
641+
match context.command_queue.pop() {
642+
Some(PlaybackCommand::SetBuffer {
643+
instance_id,
644+
buffer: scheduled_buffer,
645+
}) => {
646+
assert_eq!(instance_id, 1);
647+
assert_eq!(scheduled_buffer.as_ref(), &buffer);
648+
}
649+
other => {
650+
panic!("expected SetBuffer command, got {other:?}");
651+
}
652+
}
653+
assert!(matches!(
654+
context.command_queue.pop(),
655+
Some(PlaybackCommand::Play { instance_id: 1 })
656+
));
657+
assert!(context.command_queue.pop().is_none());
658+
return;
659+
}
660+
661+
/// `play_sound` MUST restore previous state when the queue is full.
662+
#[test]
663+
fn play_sound_restores_state_when_queue_full_for_set_buffer() {
664+
let mut context = create_test_context(48_000, 2);
665+
let buffer = create_test_sound_buffer(48_000, 2, 4);
666+
667+
context.shared_state.set_active_instance_id(9);
668+
context.shared_state.set_state(PlaybackState::Paused);
669+
670+
fill_command_queue(&context.command_queue);
671+
672+
let result = context.play_sound(&buffer);
673+
assert!(matches!(result, Err(AudioError::Platform { .. })));
674+
675+
assert_eq!(context.shared_state.active_instance_id(), 9);
676+
assert_eq!(context.shared_state.state(), PlaybackState::Paused);
677+
return;
678+
}
679+
680+
/// `play_sound` MUST restore previous state when play cannot be enqueued.
681+
#[test]
682+
fn play_sound_restores_state_when_queue_full_for_play() {
683+
let mut context = create_test_context(48_000, 2);
684+
let buffer = create_test_sound_buffer(48_000, 2, 4);
685+
686+
context.shared_state.set_active_instance_id(3);
687+
context.shared_state.set_state(PlaybackState::Paused);
688+
689+
fill_command_queue(&context.command_queue);
690+
let _first_popped = context.command_queue.pop();
691+
let _second_popped = context.command_queue.pop();
692+
693+
let result = context.play_sound(&buffer);
694+
assert!(matches!(result, Err(AudioError::Platform { .. })));
695+
696+
assert_eq!(context.shared_state.active_instance_id(), 3);
697+
assert_eq!(context.shared_state.state(), PlaybackState::Paused);
698+
return;
699+
}
700+
701+
/// Instance ids MUST wrap without using id `0`.
702+
#[test]
703+
fn play_sound_instance_id_wraps_to_one() {
704+
let mut context = create_test_context(48_000, 2);
705+
context.next_instance_id = u64::MAX;
706+
707+
let buffer = create_test_sound_buffer(48_000, 2, 4);
708+
709+
let instance = context.play_sound(&buffer).expect("must play sound");
710+
assert_eq!(instance.instance_id, u64::MAX);
711+
assert_eq!(context.next_instance_id, 1);
712+
return;
713+
}
526714
}

0 commit comments

Comments
 (0)