//! Include-based coverage for microphone source-selection helpers. //! //! Scope: include `client/src/input/microphone.rs` and exercise Pulse source //! parsing + fallback behavior without requiring a live audio stack. //! Targets: `client/src/input/microphone.rs`. //! Why: source selection regressions should be caught with deterministic tests. #[allow(warnings)] mod microphone_include_contract { include!(env!("LESAVKA_CLIENT_MICROPHONE_SRC")); use serial_test::serial; use std::fs; use std::os::unix::fs::PermissionsExt; use std::path::Path; use temp_env::with_var; use tempfile::tempdir; fn write_executable(dir: &Path, name: &str, body: &str) { let path = dir.join(name); fs::write(&path, body).expect("write script"); let mut perms = fs::metadata(&path).expect("metadata").permissions(); perms.set_mode(0o755); fs::set_permissions(path, perms).expect("chmod"); } fn with_fake_command(name: &str, script_body: &str, f: impl FnOnce()) { let dir = tempdir().expect("tempdir"); write_executable(dir.path(), name, script_body); let prior = std::env::var("PATH").unwrap_or_default(); let merged = if prior.is_empty() { dir.path().display().to_string() } else { format!("{}:{prior}", dir.path().display()) }; with_var("PATH", Some(merged), f); } fn with_fake_pactl(script_body: &str, f: impl FnOnce()) { with_fake_command("pactl", script_body, f); } fn with_fake_pw_dump(script_body: &str, f: impl FnOnce()) { with_fake_command("pw-dump", script_body, f); } #[test] #[serial] fn pulse_source_by_substr_matches_expected_device_name() { let script = r#"#!/usr/bin/env sh if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then echo "0 alsa_input.pci.monitor module-alsa-card.c s16le 2ch 48000Hz RUNNING" echo "1 alsa_input.usb-Mic_1234-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz IDLE" exit 0 fi exit 0 "#; with_fake_pactl(script, || { let src = MicrophoneCapture::pulse_source_by_substr("Mic_1234").expect("matching source"); assert_eq!(src, "alsa_input.usb-Mic_1234-00.analog-stereo"); }); } #[test] #[serial] fn pulse_source_desc_formats_selected_non_monitor_source() { let script = r#"#!/usr/bin/env sh if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then echo "0 alsa_input.pci.monitor module-alsa-card.c s16le 2ch 48000Hz RUNNING" echo "1 alsa_input.usb-DeskMic_5678-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz IDLE" exit 0 fi exit 0 "#; with_fake_pactl(script, || { let source = MicrophoneCapture::pulse_source_by_substr("DeskMic_5678").expect("matching source"); let desc = MicrophoneCapture::pulse_source_desc(Some(&source)); assert!( desc.contains("pulsesrc device=alsa_input.usb-DeskMic_5678-00.analog-stereo"), "expected escaped non-monitor source argument: {desc}" ); }); } #[test] #[serial] fn pulse_source_by_substr_returns_none_when_pactl_is_unavailable() { with_var("PATH", Some("/definitely/missing/path"), || { assert!(MicrophoneCapture::pulse_source_by_substr("anything").is_none()); assert_eq!( MicrophoneCapture::pulse_source_desc(None), "pulsesrc do-timestamp=true" ); }); } #[test] fn pipewire_source_desc_formats_selected_and_default_sources() { let selected = MicrophoneCapture::pipewire_source_desc(Some("alsa input/Desk Mic")); assert!( selected.contains("pipewiresrc target-object='alsa input/Desk Mic'"), "expected shell-escaped PipeWire target: {selected}" ); assert_eq!( MicrophoneCapture::pipewire_source_desc(Some(" ")), "pipewiresrc do-timestamp=true" ); assert_eq!( MicrophoneCapture::pipewire_source_desc(None), "pipewiresrc do-timestamp=true" ); } #[test] #[serial] fn pipewire_source_by_substr_prefers_audio_sources_and_skips_monitors() { let script = r#"#!/usr/bin/env sh cat <<'JSON' [ {"info":{"props":{"media.class":"Audio/Sink","node.name":"alsa_output.usb-headphones"}}}, {"info":{"props":{"media.class":"Audio/Source","node.name":"alsa_input.usb-DeskMic.monitor"}}}, {"info":{"props":{"media.class":"Audio/Source","node.name":"alsa_input.usb-DeskMic"}}}, {"info":{"props":{"media.class":"Audio/Source","node.nick":"Fallback Nick Mic"}}} ] JSON "#; with_fake_pw_dump(script, || { assert_eq!( MicrophoneCapture::pipewire_source_by_substr("DeskMic").as_deref(), Some("alsa_input.usb-DeskMic") ); assert_eq!( MicrophoneCapture::pipewire_source_by_substr("Fallback Nick").as_deref(), Some("Fallback Nick Mic") ); assert!(MicrophoneCapture::pipewire_source_by_substr("missing").is_none()); }); } #[test] fn default_source_desc_selects_a_valid_gstreamer_source_description() { gst::init().ok(); let desc = MicrophoneCapture::default_source_desc(); assert!( desc == "pipewiresrc do-timestamp=true" || desc == "pulsesrc do-timestamp=true", "default source should stay a simple PipeWire/Pulse source: {desc}" ); } #[test] #[serial] fn mic_gain_env_defaults_and_clamps_for_uplink_gain() { with_var("LESAVKA_MIC_GAIN", None::<&str>, || { assert_eq!(mic_gain_from_env(), 1.0); }); with_var("LESAVKA_MIC_GAIN", Some("2.75"), || { assert_eq!(mic_gain_from_env(), 2.75); }); with_var("LESAVKA_MIC_GAIN", Some("99"), || { assert_eq!(mic_gain_from_env(), 4.0); }); with_var("LESAVKA_MIC_GAIN", Some("-1"), || { assert_eq!(mic_gain_from_env(), 0.0); }); with_var("LESAVKA_MIC_GAIN", Some("bad"), || { assert_eq!(mic_gain_from_env(), 1.0); }); } #[test] fn microphone_pipeline_desc_adds_level_tap_only_when_requested() { assert!(parser_for_encoder("opusenc").contains("audio/x-opus")); assert!(parser_for_encoder("avenc_aac").contains("audio/mpeg")); let with_tap = microphone_pipeline_desc( "audiotestsrc is-live=true", "opusenc", parser_for_encoder("opusenc"), 2.5, true, ); assert!( with_tap .contains("audiotestsrc is-live=true ! audioconvert ! audioresample ! audio/x-raw") ); assert!(!with_tap.contains("audiotestsrc is-live=true ! audio/x-raw")); assert!(with_tap.contains("tee name=t")); assert!(with_tap.contains("appsink name=level_sink")); assert!(with_tap.contains("volume name=mic_input_gain volume=2.500")); let without_tap = microphone_pipeline_desc( "audiotestsrc is-live=true", "avenc_aac", parser_for_encoder("avenc_aac"), 1.0, false, ); assert!(!without_tap.contains("level_sink")); assert!(without_tap.contains("queue max-size-buffers=100 leaky=downstream")); } #[test] fn mic_gain_control_reads_first_token_and_clamps() { let dir = tempdir().expect("tempdir"); let path = dir.path().join("mic-gain.control"); fs::write(&path, "3.250 nonce\n").expect("write gain"); assert_eq!(read_mic_gain_control(&path), Some(3.25)); fs::write(&path, "20.0 nonce\n").expect("write clamped gain"); assert_eq!(read_mic_gain_control(&path), Some(4.0)); fs::write(&path, "bad nonce\n").expect("write invalid gain"); assert_eq!(read_mic_gain_control(&path), None); } #[test] #[serial] fn mic_level_tap_env_and_payload_helpers_are_stable() { with_var("LESAVKA_UPLINK_MIC_LEVEL", None::<&str>, || { assert!(mic_level_tap_path().is_none()); }); let dir = tempdir().expect("tempdir"); let path = dir.path().join("uplink-mic-level.value"); with_var( "LESAVKA_UPLINK_MIC_LEVEL", Some(path.to_string_lossy().to_string()), || { assert_eq!(mic_level_tap_path().as_deref(), Some(path.as_path())); }, ); assert_eq!(pcm_peak_fraction(&0_i16.to_le_bytes()), 0.0); assert!(pcm_peak_fraction(&i16::MAX.to_le_bytes()) > 0.99); write_mic_level_tap(&path, 0.375).expect("write level tap"); assert_eq!( fs::read_to_string(&path).expect("read level tap").trim(), "0.375000" ); } #[test] #[serial] fn mic_gain_control_returns_without_env() { gst::init().ok(); let volume = gst::ElementFactory::make("volume") .build() .expect("volume element"); volume.set_property("volume", 1.75_f64); with_var("LESAVKA_MIC_GAIN_CONTROL", None::<&str>, || { maybe_spawn_mic_gain_control(volume.clone()); }); assert_eq!(volume.property::("volume"), 1.75); } #[test] #[serial] fn mic_gain_control_updates_volume_element_live() { gst::init().ok(); let dir = tempdir().expect("tempdir"); let path = dir.path().join("mic-gain.control"); fs::write(&path, "2.500 nonce\n").expect("write gain"); let volume = gst::ElementFactory::make("volume") .build() .expect("volume element"); with_var( "LESAVKA_MIC_GAIN_CONTROL", Some(path.to_string_lossy().to_string()), || { maybe_spawn_mic_gain_control(volume.clone()); for _ in 0..20 { if (volume.property::("volume") - 2.5).abs() < 0.001 { return; } std::thread::sleep(std::time::Duration::from_millis(25)); } }, ); assert!( (volume.property::("volume") - 2.5).abs() < 0.001, "live mic gain control should update the GStreamer volume" ); } #[test] fn pull_returns_none_for_empty_appsink() { gst::init().ok(); let sink: gst_app::AppSink = gst::ElementFactory::make("appsink") .build() .expect("appsink") .downcast::() .expect("appsink cast"); let running = std::sync::Arc::new(AtomicBool::new(true)); let cap = MicrophoneCapture { pipeline: gst::Pipeline::new(), sink, level_tap_running: Some(std::sync::Arc::clone(&running)), }; assert!( cap.pull().is_none(), "empty appsink should produce no packet" ); drop(cap); assert!(!running.load(AtomicOrdering::Acquire)); } #[test] fn spawned_mic_level_tap_publishes_peak_from_appsink() { gst::init().ok(); let dir = tempdir().expect("tempdir"); let path = dir.path().join("mic-level.value"); let pipeline: gst::Pipeline = gst::parse::launch( "appsrc name=src is-live=true format=time caps=audio/x-raw,format=S16LE,channels=2,rate=48000 ! \ appsink name=sink emit-signals=false sync=false max-buffers=4 drop=true", ) .expect("pipeline") .downcast() .expect("pipeline cast"); let src: gst_app::AppSrc = pipeline .by_name("src") .expect("appsrc") .downcast() .expect("appsrc cast"); let sink: gst_app::AppSink = pipeline .by_name("sink") .expect("appsink") .downcast() .expect("appsink cast"); pipeline.set_state(gst::State::Playing).expect("playing"); let running = spawn_mic_level_tap(sink, path.clone()); src.push_buffer(gst::Buffer::from_slice(i16::MAX.to_le_bytes().repeat(4))) .expect("push buffer"); for _ in 0..20 { if let Ok(raw) = fs::read_to_string(&path) { let level = raw.trim().parse::().expect("level"); assert!(level > 0.99); running.store(false, AtomicOrdering::Release); let _ = pipeline.set_state(gst::State::Null); return; } std::thread::sleep(std::time::Duration::from_millis(25)); } running.store(false, AtomicOrdering::Release); let _ = pipeline.set_state(gst::State::Null); panic!("microphone level tap did not publish a value"); } #[test] #[cfg(coverage)] #[serial] fn microphone_capture_with_level_tap_uses_the_same_uplink_pipeline() { gst::init().ok(); let dir = tempdir().expect("tempdir"); let level_path = dir.path().join("uplink-mic-level.value"); with_var("LESAVKA_MIC_SOURCE", None::<&str>, || { with_var( "LESAVKA_MIC_TEST_SOURCE_DESC", Some("audiotestsrc is-live=true wave=sine freq=440".to_string()), || { with_var( "LESAVKA_UPLINK_MIC_LEVEL", Some(level_path.to_string_lossy().to_string()), || { let cap = MicrophoneCapture::new().expect("synthetic mic capture"); assert!(cap.level_tap_running.is_some()); }, ); }, ); }); } #[test] fn pull_returns_packet_when_appsink_has_buffered_sample() { gst::init().ok(); let pipeline = gst::Pipeline::new(); let src = gst::ElementFactory::make("appsrc") .build() .expect("appsrc") .downcast::() .expect("appsrc cast"); let sink = gst::ElementFactory::make("appsink") .property("emit-signals", false) .property("sync", false) .build() .expect("appsink") .downcast::() .expect("appsink cast"); pipeline .add_many([ src.upcast_ref::(), sink.upcast_ref::(), ]) .expect("add appsrc/appsink"); src.link(&sink).expect("link appsrc->appsink"); pipeline.set_state(gst::State::Playing).ok(); let mut buf = gst::Buffer::from_slice(vec![1_u8, 2, 3, 4]); buf.get_mut() .expect("buffer mut") .set_pts(Some(gst::ClockTime::from_useconds(321))); src.push_buffer(buf).expect("push sample"); let cap = MicrophoneCapture { pipeline, sink, level_tap_running: None, }; let pkt = cap.pull().expect("audio packet"); assert_eq!(pkt.id, 0); assert_eq!(pkt.pts, 321); assert_eq!(pkt.data, vec![1, 2, 3, 4]); } #[test] #[serial] fn new_uses_requested_source_fragment_when_available() { let script = r#"#!/usr/bin/env sh if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then echo "1 alsa_input.usb-LavMic_abc-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz RUNNING" exit 0 fi exit 0 "#; with_fake_pactl(script, || { with_var("LESAVKA_MIC_SOURCE", Some("LavMic_abc"), || { let result = MicrophoneCapture::new(); if let Err(err) = result { assert!(!err.to_string().trim().is_empty()); } }); }); } #[test] #[serial] fn resolve_source_desc_prefers_pipewire_named_source_when_available() { if !MicrophoneCapture::pipewire_source_available() { return; } let script = r#"#!/usr/bin/env sh cat <<'JSON' [ {"info":{"props":{"media.class":"Audio/Source","node.name":"alsa_input.usb-UpstreamMic"}}} ] JSON "#; with_fake_pw_dump(script, || { let desc = MicrophoneCapture::resolve_source_desc("UpstreamMic").expect("pipewire source"); assert!(desc.contains("pipewiresrc target-object=alsa_input.usb-UpstreamMic")); }); } #[test] #[serial] fn new_falls_back_to_default_source_when_requested_fragment_is_missing() { let script = r#"#!/usr/bin/env sh if [ "$1" = "list" ] && [ "$2" = "short" ] && [ "$3" = "sources" ]; then echo "0 alsa_input.pci.monitor module-alsa-card.c s16le 2ch 48000Hz RUNNING" echo "1 alsa_input.usb-DeskMic_777-00.analog-stereo module-alsa-card.c s16le 2ch 48000Hz IDLE" exit 0 fi exit 0 "#; with_fake_pactl(script, || { with_var("LESAVKA_MIC_SOURCE", Some("missing-fragment"), || { let result = MicrophoneCapture::new(); if let Err(err) = result { assert!(!err.to_string().trim().is_empty()); } }); }); } }