diff --git a/staging/vhost-device-sound/src/audio_backends/alsa.rs b/staging/vhost-device-sound/src/audio_backends/alsa.rs index 8b779ed..04150df 100644 --- a/staging/vhost-device-sound/src/audio_backends/alsa.rs +++ b/staging/vhost-device-sound/src/audio_backends/alsa.rs @@ -724,3 +724,512 @@ impl AudioBackend for AlsaBackend { self } } + +#[cfg(test)] +/// Utilities for temporarily setting a test-specific alsa config. +pub mod test_utils; + +#[cfg(test)] +mod tests { + use super::{test_utils::setup_alsa_conf, *}; + use crate::{stream::PcmParams, virtio_sound::*}; + + const RATES: [u8; _VIRTIO_SND_PCM_RATE_MAX as usize] = [ + virtio_sound::VIRTIO_SND_PCM_RATE_5512, + virtio_sound::VIRTIO_SND_PCM_RATE_8000, + virtio_sound::VIRTIO_SND_PCM_RATE_11025, + virtio_sound::VIRTIO_SND_PCM_RATE_16000, + virtio_sound::VIRTIO_SND_PCM_RATE_22050, + virtio_sound::VIRTIO_SND_PCM_RATE_32000, + virtio_sound::VIRTIO_SND_PCM_RATE_44100, + virtio_sound::VIRTIO_SND_PCM_RATE_48000, + virtio_sound::VIRTIO_SND_PCM_RATE_64000, + virtio_sound::VIRTIO_SND_PCM_RATE_88200, + virtio_sound::VIRTIO_SND_PCM_RATE_96000, + virtio_sound::VIRTIO_SND_PCM_RATE_176400, + virtio_sound::VIRTIO_SND_PCM_RATE_192000, + virtio_sound::VIRTIO_SND_PCM_RATE_384000, + ]; + + const FORMATS: [u8; _VIRTIO_SND_PCM_FMT_MAX as usize] = [ + virtio_sound::VIRTIO_SND_PCM_FMT_IMA_ADPCM, + virtio_sound::VIRTIO_SND_PCM_FMT_MU_LAW, + virtio_sound::VIRTIO_SND_PCM_FMT_A_LAW, + virtio_sound::VIRTIO_SND_PCM_FMT_S8, + virtio_sound::VIRTIO_SND_PCM_FMT_U8, + virtio_sound::VIRTIO_SND_PCM_FMT_S16, + virtio_sound::VIRTIO_SND_PCM_FMT_U16, + virtio_sound::VIRTIO_SND_PCM_FMT_S18_3, + virtio_sound::VIRTIO_SND_PCM_FMT_U18_3, + virtio_sound::VIRTIO_SND_PCM_FMT_S20_3, + virtio_sound::VIRTIO_SND_PCM_FMT_U20_3, + virtio_sound::VIRTIO_SND_PCM_FMT_S24_3, + virtio_sound::VIRTIO_SND_PCM_FMT_U24_3, + virtio_sound::VIRTIO_SND_PCM_FMT_S20, + virtio_sound::VIRTIO_SND_PCM_FMT_U20, + virtio_sound::VIRTIO_SND_PCM_FMT_S24, + virtio_sound::VIRTIO_SND_PCM_FMT_U24, + virtio_sound::VIRTIO_SND_PCM_FMT_S32, + virtio_sound::VIRTIO_SND_PCM_FMT_U32, + virtio_sound::VIRTIO_SND_PCM_FMT_FLOAT, + virtio_sound::VIRTIO_SND_PCM_FMT_FLOAT64, + virtio_sound::VIRTIO_SND_PCM_FMT_DSD_U8, + virtio_sound::VIRTIO_SND_PCM_FMT_DSD_U16, + virtio_sound::VIRTIO_SND_PCM_FMT_DSD_U32, + virtio_sound::VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME, + ]; + + #[test] + fn test_alsa_trait_impls() { + crate::init_logger(); + let _harness = setup_alsa_conf(); + + let _: alsa::Direction = Direction::Output.into(); + let _: alsa::Direction = Direction::Input.into(); + + let backend = AlsaBackend::new(Default::default()); + #[allow(clippy::redundant_clone)] + let _ = backend.clone(); + + _ = format!("{:?}", backend); + } + + #[test] + fn test_alsa_ops() { + crate::init_logger(); + let _harness = setup_alsa_conf(); + + let streams = Arc::new(RwLock::new(vec![ + Stream::default(), + Stream { + id: 1, + direction: Direction::Input, + ..Stream::default() + }, + ])); + let backend = AlsaBackend::new(streams); + let request = VirtioSndPcmSetParams { + hdr: VirtioSoundPcmHeader { + stream_id: 0.into(), + hdr: VirtioSoundHeader { code: 0.into() }, + }, + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + features: 0.into(), + buffer_bytes: 8192.into(), + period_bytes: 4096.into(), + padding: 0, + }; + backend.set_parameters(0, request).unwrap(); + backend.prepare(0).unwrap(); + backend.start(0).unwrap(); + backend.write(0).unwrap(); + backend.read(0).unwrap(); + backend.stop(0).unwrap(); + backend.release(0).unwrap(); + } + + #[test] + fn test_alsa_invalid_stream_id() { + crate::init_logger(); + let _harness = setup_alsa_conf(); + + let streams = Arc::new(RwLock::new(vec![ + Stream::default(), + Stream { + id: 1, + direction: Direction::Input, + ..Stream::default() + }, + ])); + let backend = AlsaBackend::new(streams); + let request = VirtioSndPcmSetParams { + hdr: VirtioSoundPcmHeader { + stream_id: 3.into(), + hdr: VirtioSoundHeader { code: 0.into() }, + }, + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + features: 0.into(), + buffer_bytes: 8192.into(), + period_bytes: 4096.into(), + padding: 0, + }; + backend.set_parameters(3, request).unwrap_err(); + backend.prepare(3).unwrap_err(); + backend.start(3).unwrap_err(); + backend.write(3).unwrap_err(); + backend.read(3).unwrap_err(); + backend.stop(3).unwrap_err(); + backend.release(3).unwrap_err(); + } + + #[test] + fn test_alsa_invalid_state_transitions() { + crate::init_logger(); + let _harness = setup_alsa_conf(); + + let streams = Arc::new(RwLock::new(vec![ + Stream::default(), + Stream { + id: 1, + direction: Direction::Input, + ..Stream::default() + }, + ])); + let request = VirtioSndPcmSetParams { + hdr: VirtioSoundPcmHeader { + stream_id: 3.into(), + hdr: VirtioSoundHeader { code: 0.into() }, + }, + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + features: 0.into(), + buffer_bytes: 8192.into(), + period_bytes: 4096.into(), + padding: 0, + }; + { + let backend = AlsaBackend::new(streams.clone()); + + // Invalid, but we allow it. + backend.stop(0).unwrap(); + // Invalid, but we don't allow it. + backend.release(0).unwrap_err(); + backend.start(0).unwrap_err(); + backend.release(0).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams.clone()); + + // set_parameters -> set_parameters | VALID + backend.set_parameters(0, request).unwrap(); + + // set_parameters -> prepare | VALID + backend.prepare(0).unwrap(); + + // Invalid, but we allow it. + // prepare -> stop | INVALID + backend.stop(0).unwrap(); + // prepare -> release | VALID + backend.release(0).unwrap(); + + // release -> start | INVALID + backend.start(0).unwrap_err(); + // release -> stop | VALID + backend.stop(0).unwrap(); + // release -> prepare | VALID + backend.prepare(0).unwrap(); + + // prepare -> start | VALID + backend.start(0).unwrap(); + + // start -> start | INVALID + backend.start(0).unwrap_err(); + // start -> set_parameters | INVALID + backend.set_parameters(0, request).unwrap_err(); + // start -> prepare | INVALID + backend.prepare(0).unwrap_err(); + // start -> release | INVALID + backend.release(0).unwrap_err(); + // start -> stop | VALID + backend.stop(0).unwrap(); + // stop -> start | VALID + backend.start(0).unwrap(); + // start -> stop | VALID + backend.stop(0).unwrap(); + // stop -> prepare | INVALID + backend.prepare(0).unwrap_err(); + // stop -> set_parameters | INVALID + backend.set_parameters(0, request).unwrap_err(); + // stop -> release | VALID + backend.release(0).unwrap(); + } + + // Redundant checks? Oh well. + // + // Generated with: + // + // ```python + // import itertools + // states = ["SetParameters", "Prepare", "Release", "Start", "Stop"] + // combs = set(itertools.product(states, repeat=2)) + // ``` + { + let backend = AlsaBackend::new(streams.clone()); + backend.set_parameters(0, request).unwrap(); + backend.prepare(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.prepare(0).unwrap(); + backend.stop(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.start(0).unwrap(); + backend.start(0).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.prepare(0).unwrap_err(); + backend.start(0).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.stop(0).unwrap(); + backend.release(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.stop(0).unwrap(); + backend.prepare(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.release(0).unwrap(); + backend.set_parameters(0, request).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.start(0).unwrap_err(); + backend.set_parameters(0, request).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.prepare(0).unwrap(); + backend.set_parameters(0, request).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.release(0).unwrap_err(); + backend.read(0).unwrap_err(); + backend.write(0).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.set_parameters(0, request).unwrap(); + backend.stop(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.release(0).unwrap_err(); + backend.prepare(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.set_parameters(0, request).unwrap(); + backend.start(0).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.start(0).unwrap_err(); + backend.release(0).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.prepare(0).unwrap(); + backend.release(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.start(0).unwrap_err(); + backend.prepare(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.stop(0).unwrap(); + backend.stop(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.prepare(0).unwrap(); + backend.prepare(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.stop(0).unwrap(); + backend.start(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.set_parameters(0, request).unwrap_err(); + backend.release(0).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.release(0).unwrap_err(); + backend.stop(0).unwrap(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.stop(0).unwrap(); + backend.set_parameters(0, request).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams.clone()); + backend.release(0).unwrap(); + backend.start(0).unwrap_err(); + } + { + let backend = AlsaBackend::new(streams); + backend.start(0).unwrap_err(); + backend.stop(0).unwrap(); + } + } + + #[test] + fn test_alsa_worker() { + crate::init_logger(); + let _harness = setup_alsa_conf(); + + let streams = Arc::new(RwLock::new(vec![ + Stream::default(), + Stream { + id: 1, + direction: Direction::Input, + ..Stream::default() + }, + ])); + let (sender, receiver) = channel(); + let pcm = Arc::new(Mutex::new( + PCM::new("null", Direction::Output.into(), false).unwrap(), + )); + + let mtx = Arc::clone(&pcm); + let streams = Arc::clone(&streams); + let _handle = + thread::spawn(move || alsa_worker(mtx.clone(), streams.clone(), &receiver, 0)); + sender.send(false).unwrap(); + } + + #[test] + fn test_alsa_valid_parameters() { + crate::init_logger(); + let _harness = setup_alsa_conf(); + + let streams = Arc::new(RwLock::new(vec![ + Stream::default(), + Stream { + id: 1, + direction: Direction::Input, + ..Stream::default() + }, + ])); + let mut request = VirtioSndPcmSetParams { + hdr: VirtioSoundPcmHeader { + stream_id: 0.into(), + hdr: VirtioSoundHeader { code: 0.into() }, + }, + format: VIRTIO_SND_PCM_FMT_S16, + rate: VIRTIO_SND_PCM_RATE_44100, + channels: 2, + features: 0.into(), + buffer_bytes: 8192.into(), + period_bytes: 4096.into(), + padding: 0, + }; + + for rate in RATES + .iter() + .cloned() + .filter(|rt| ((1 << *rt) & crate::SUPPORTED_RATES) > 0) + { + request.rate = rate; + let backend = AlsaBackend::new(streams.clone()); + backend.set_parameters(0, request).unwrap(); + } + + for rate in RATES + .iter() + .cloned() + .filter(|rt| ((1 << *rt) & crate::SUPPORTED_RATES) == 0) + { + request.rate = rate; + let backend = AlsaBackend::new(streams.clone()); + backend.set_parameters(0, request).unwrap_err(); + } + request.rate = VIRTIO_SND_PCM_RATE_44100; + + for format in FORMATS + .iter() + .cloned() + .filter(|fmt| ((1 << *fmt) & crate::SUPPORTED_FORMATS) > 0) + { + request.format = format; + let backend = AlsaBackend::new(streams.clone()); + backend.set_parameters(0, request).unwrap(); + } + + for format in FORMATS + .iter() + .cloned() + .filter(|fmt| ((1 << *fmt) & crate::SUPPORTED_FORMATS) == 0) + { + request.format = format; + let backend = AlsaBackend::new(streams.clone()); + backend.set_parameters(0, request).unwrap_err(); + } + + { + for format in FORMATS + .iter() + .cloned() + .filter(|fmt| ((1 << *fmt) & crate::SUPPORTED_FORMATS) > 0) + { + let streams = Arc::new(RwLock::new(vec![Stream { + params: PcmParams { + format, + ..PcmParams::default() + }, + ..Stream::default() + }])); + let pcm = Arc::new(Mutex::new( + PCM::new("null", Direction::Output.into(), false).unwrap(), + )); + update_pcm(&pcm, 0, &streams).unwrap(); + } + } + } + + #[test] + #[should_panic(expected = "unreachable")] + fn test_alsa_invalid_rate() { + crate::init_logger(); + let _harness = setup_alsa_conf(); + + let streams = Arc::new(RwLock::new(vec![Stream { + params: PcmParams { + rate: _VIRTIO_SND_PCM_RATE_MAX, + ..PcmParams::default() + }, + ..Stream::default() + }])); + let pcm = Arc::new(Mutex::new( + PCM::new("null", Direction::Output.into(), false).unwrap(), + )); + update_pcm(&pcm, 0, &streams).unwrap(); + } + + #[test] + #[should_panic(expected = "unreachable")] + fn test_alsa_invalid_fmt() { + crate::init_logger(); + let _harness = setup_alsa_conf(); + + let streams = Arc::new(RwLock::new(vec![Stream { + params: PcmParams { + format: _VIRTIO_SND_PCM_FMT_MAX, + ..PcmParams::default() + }, + ..Stream::default() + }])); + let pcm = Arc::new(Mutex::new( + PCM::new("null", Direction::Output.into(), false).unwrap(), + )); + update_pcm(&pcm, 0, &streams).unwrap(); + } +} diff --git a/staging/vhost-device-sound/src/audio_backends/alsa/test_utils.rs b/staging/vhost-device-sound/src/audio_backends/alsa/test_utils.rs new file mode 100644 index 0000000..0d4f0cd --- /dev/null +++ b/staging/vhost-device-sound/src/audio_backends/alsa/test_utils.rs @@ -0,0 +1,128 @@ +// Manos Pitsidianakis +// SPDX-License-Identifier: Apache-2.0 or BSD-3-Clause + +use std::{ + path::PathBuf, + sync::{ + atomic::{AtomicU8, Ordering}, + Arc, Mutex, Once, + }, +}; + +use tempfile::{tempdir, TempDir}; + +static mut TEST_HARNESS: Option = None; +static INIT_ALSA_CONF: Once = Once::new(); + +#[must_use] +pub fn setup_alsa_conf() -> AlsaTestHarnessRef<'static> { + INIT_ALSA_CONF.call_once(|| + // SAFETY: + // This is only called once, because of.. `Once`, so it's safe to + // access the static value mutably. + unsafe { + TEST_HARNESS = Some(AlsaTestHarness::new()); + }); + let retval = AlsaTestHarnessRef( + // SAFETY: + // The unsafe { } block is needed because TEST_HARNESS is a mutable static. The inner + // operations are protected by atomics. + unsafe { TEST_HARNESS.as_ref().unwrap() }, + ); + retval.0.inc_ref(); + retval +} + +/// The alsa test harness. It must only be constructed via +/// `AlsaTestHarness::new()`. +#[non_exhaustive] +pub struct AlsaTestHarness { + pub tempdir: Arc>>, + pub conf_path: PathBuf, + pub ref_count: AtomicU8, +} + +/// Ref counted alsa test harness ref. +#[repr(transparent)] +#[non_exhaustive] +pub struct AlsaTestHarnessRef<'a>(&'a AlsaTestHarness); + +impl<'a> Drop for AlsaTestHarnessRef<'a> { + fn drop(&mut self) { + self.0.dec_ref(); + } +} + +impl AlsaTestHarness { + pub fn new() -> Self { + let tempdir = tempdir().unwrap(); + let conf_path = tempdir.path().join("alsa.conf"); + + std::fs::write( + &conf_path, + b"pcm.!default {\n type null \n }\n\nctl.!default {\n type null\n }\n\npcm.null {\n type null \n }\n\nctl.null {\n type null\n }\n", + ).unwrap(); + + std::env::set_var("ALSA_CONFIG_PATH", &conf_path); + println!( + "INFO: setting ALSA_CONFIG_PATH={} in PID {} and TID {:?}", + conf_path.display(), + std::process::id(), + std::thread::current().id() + ); + + Self { + tempdir: Arc::new(Mutex::new(Some(tempdir))), + conf_path, + ref_count: 0.into(), + } + } + + #[inline] + pub fn inc_ref(&self) { + let old_val = self.ref_count.fetch_add(1, Ordering::SeqCst); + assert!( + old_val != u8::MAX, + "ref_count overflowed on 8bits when increasing by 1" + ); + } + + #[inline] + pub fn dec_ref(&self) { + let old_val = self.ref_count.fetch_sub(1, Ordering::SeqCst); + if old_val == 1 { + let mut lck = self.tempdir.lock().unwrap(); + println!( + "INFO: unsetting ALSA_CONFIG_PATH={} in PID {} and TID {:?}", + self.conf_path.display(), + std::process::id(), + std::thread::current().id() + ); + std::env::remove_var("ALSA_CONFIG_PATH"); + _ = lck.take(); + } + } +} + +impl Drop for AlsaTestHarness { + fn drop(&mut self) { + let ref_count = self.ref_count.load(Ordering::SeqCst); + if ref_count != 0 { + println!( + "ERROR: ref_count is {ref_count} when dropping {}", + stringify!(AlsaTestHarness) + ); + } + if self + .tempdir + .lock() + .map(|mut l| l.take().is_some()) + .unwrap_or(false) + { + println!( + "ERROR: tempdir held a value when dropping {}", + stringify!(AlsaTestHarness) + ); + } + } +} diff --git a/staging/vhost-device-sound/src/virtio_sound.rs b/staging/vhost-device-sound/src/virtio_sound.rs index bb9744c..a666b3f 100644 --- a/staging/vhost-device-sound/src/virtio_sound.rs +++ b/staging/vhost-device-sound/src/virtio_sound.rs @@ -89,6 +89,7 @@ pub const VIRTIO_SND_PCM_FMT_DSD_U8: u8 = 21; pub const VIRTIO_SND_PCM_FMT_DSD_U16: u8 = 22; pub const VIRTIO_SND_PCM_FMT_DSD_U32: u8 = 23; pub const VIRTIO_SND_PCM_FMT_IEC958_SUBFRAME: u8 = 24; +pub(crate) const _VIRTIO_SND_PCM_FMT_MAX: u8 = 25; // supported PCM frame rates @@ -106,6 +107,7 @@ pub const VIRTIO_SND_PCM_RATE_96000: u8 = 10; pub const VIRTIO_SND_PCM_RATE_176400: u8 = 11; pub const VIRTIO_SND_PCM_RATE_192000: u8 = 12; pub const VIRTIO_SND_PCM_RATE_384000: u8 = 13; +pub(crate) const _VIRTIO_SND_PCM_RATE_MAX: u8 = 14; // standard channel position definition