//! Integration coverage for server audio capture/sink plumbing. //! //! Scope: compile the split server audio module and exercise public audio //! constructors/helpers across deterministic error and smoke paths. //! Targets: `server/src/audio.rs` plus `server/src/audio/*`. //! Why: audio pipeline setup is branchy and should stay stable without requiring //! physical ALSA/UAC hardware in CI. #[path = "../../server/src/audio.rs"] #[allow(warnings)] mod server_audio_contract; mod tests { use super::server_audio_contract::{ClipTap, Voice, ear, start_pipeline_or_reset}; #[cfg(coverage)] use futures_util::StreamExt; use gstreamer as gst; use gstreamer::prelude::*; use lesavka_common::lesavka::AudioPacket; use serial_test::serial; const AUDIO_SRC: &str = concat!( include_str!("../../server/src/audio.rs"), include_str!("../../server/src/audio/ear_capture.rs"), include_str!("../../server/src/audio/voice_input.rs"), ); fn source_index(needle: &str) -> usize { AUDIO_SRC .find(needle) .unwrap_or_else(|| panic!("missing source marker: {needle}")) } #[test] fn failed_pipeline_start_contract_resets_and_does_not_spawn_bus_first() { assert!(AUDIO_SRC.contains("let _ = pipeline.set_state(gst::State::Null);")); assert!(AUDIO_SRC.contains("impl Drop for Voice")); assert!( source_index("start_pipeline_or_reset(&pipeline, \"starting audio pipeline\")?") < source_index("spawn_pipeline_bus_logger(bus, \"audio\"") ); assert!( source_index("start_pipeline_or_reset(&pipeline, \"starting voice pipeline\")?") < source_index("spawn_pipeline_bus_logger(bus, \"voice\"") ); } #[test] #[serial] fn start_pipeline_or_reset_starts_empty_pipeline() { let _ = gst::init(); let pipeline = gst::Pipeline::new(); start_pipeline_or_reset(&pipeline, "starting contract pipeline") .expect("empty pipeline should enter playing"); assert_eq!(pipeline.current_state(), gst::State::Playing); let _ = pipeline.set_state(gst::State::Null); } #[cfg(coverage)] #[test] #[serial] fn start_pipeline_or_reset_forced_failure_resets_pipeline() { let _ = gst::init(); let pipeline = gst::Pipeline::new(); temp_env::with_var("LESAVKA_TEST_FORCE_PIPELINE_START_ERROR", Some("1"), || { let err = start_pipeline_or_reset(&pipeline, "starting forced contract pipeline") .expect_err("forced coverage error"); assert!(err.to_string().contains("forced test failure")); }); assert_eq!(pipeline.current_state(), gst::State::Null); } #[test] #[serial] fn ear_rejects_malformed_pipeline_device_string() { let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(ear("hw:UAC2Gadget,0\" ! broken-pipe", 0)); assert!(result.is_err(), "malformed device string should fail parse"); } #[test] #[serial] fn ear_missing_device_path_is_stable() { let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(ear("hw:DefinitelyMissingDevice,0", 0)); if let Err(err) = result { assert!(!err.to_string().trim().is_empty()); } } #[test] #[serial] fn ear_existing_non_audio_node_reaches_runtime_paths() { let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(async { tokio::time::timeout(std::time::Duration::from_millis(250), ear("/dev/null", 0)).await }); if let Ok(Ok(stream)) = result { drop(stream) } } #[test] fn clip_tap_feed_flush_and_drop_are_stable() { let mut tap = ClipTap::new("audio-contract", std::time::Duration::from_millis(1)); tap.feed(&[1, 2, 3, 4, 5]); tap.feed(&vec![9u8; 300_000]); tap.flush(); tap.flush(); // empty flush should be a no-op drop(tap); } #[test] #[serial] fn voice_constructor_and_push_finish_are_stable() { let rt = tokio::runtime::Runtime::new().expect("runtime"); let result = rt.block_on(Voice::new("hw:DefinitelyMissingDevice,0")); match result { Ok(mut voice) => { voice.push(&AudioPacket { id: 0, pts: 77, data: vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC], }); voice.finish(); } Err(err) => { assert!(!err.to_string().trim().is_empty()); } } } #[cfg(coverage)] #[test] #[serial] fn audio_stream_poll_next_is_stable_when_channel_is_closed() { let rt = tokio::runtime::Runtime::new().expect("runtime"); let polled = rt.block_on(async { let mut stream = ear("/dev/null", 0).await.expect("coverage ear stream"); stream.next().await }); assert!(polled.is_none(), "closed stream should yield None"); } }