//! 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 live_capture_clock { include!("support/live_capture_clock_shim.rs"); } #[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}" ); assert!(desc.contains("buffer-time=40000")); assert!(desc.contains("latency-time=10000")); }); } #[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 buffer-time=40000 latency-time=10000" ); }); } #[test] #[serial] fn pulse_source_desc_allows_low_latency_overrides_with_safe_clamp() { with_var("LESAVKA_MIC_PULSE_BUFFER_TIME_US", Some("20000"), || { with_var("LESAVKA_MIC_PULSE_LATENCY_TIME_US", Some("50000"), || { assert_eq!( MicrophoneCapture::pulse_source_desc(None), "pulsesrc do-timestamp=true buffer-time=20000 latency-time=20000" ); }); }); } #[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 buffer-time=40000 latency-time=10000", "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() { let with_tap = microphone_pipeline_desc("audiotestsrc is-live=true", 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", 1.0, false); assert!(!without_tap.contains("level_sink")); assert!( without_tap .contains("queue max-size-buffers=8 max-size-time=80000000 leaky=downstream") ); assert!(without_tap.contains("appsink name=asink emit-signals=true max-buffers=8")); } }