2026-04-13 02:52:32 -03:00
|
|
|
//! 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_pactl(script_body: &str, f: impl FnOnce()) {
|
|
|
|
|
let dir = tempdir().expect("tempdir");
|
|
|
|
|
write_executable(dir.path(), "pactl", 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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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, || {
|
2026-04-14 23:03:18 -03:00
|
|
|
let src =
|
|
|
|
|
MicrophoneCapture::pulse_source_by_substr("Mic_1234").expect("matching source");
|
2026-04-13 02:52:32 -03:00
|
|
|
assert_eq!(src, "alsa_input.usb-Mic_1234-00.analog-stereo");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn default_source_arg_prefers_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 arg = MicrophoneCapture::default_source_arg();
|
|
|
|
|
assert!(
|
|
|
|
|
arg.contains("device=alsa_input.usb-DeskMic_5678-00.analog-stereo"),
|
|
|
|
|
"expected escaped non-monitor source argument"
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial]
|
|
|
|
|
fn default_source_arg_returns_empty_when_pactl_is_unavailable() {
|
|
|
|
|
with_var("PATH", Some("/definitely/missing/path"), || {
|
|
|
|
|
let arg = MicrophoneCapture::default_source_arg();
|
|
|
|
|
assert!(arg.is_empty());
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pull_returns_none_for_empty_appsink() {
|
|
|
|
|
gst::init().ok();
|
|
|
|
|
let sink: gst_app::AppSink = gst::ElementFactory::make("appsink")
|
|
|
|
|
.build()
|
|
|
|
|
.expect("appsink")
|
|
|
|
|
.downcast::<gst_app::AppSink>()
|
|
|
|
|
.expect("appsink cast");
|
|
|
|
|
let cap = MicrophoneCapture {
|
|
|
|
|
pipeline: gst::Pipeline::new(),
|
|
|
|
|
sink,
|
|
|
|
|
};
|
2026-04-14 23:03:18 -03:00
|
|
|
assert!(
|
|
|
|
|
cap.pull().is_none(),
|
|
|
|
|
"empty appsink should produce no packet"
|
|
|
|
|
);
|
2026-04-13 02:52:32 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[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::<gst_app::AppSrc>()
|
|
|
|
|
.expect("appsrc cast");
|
|
|
|
|
let sink = gst::ElementFactory::make("appsink")
|
|
|
|
|
.property("emit-signals", false)
|
|
|
|
|
.property("sync", false)
|
|
|
|
|
.build()
|
|
|
|
|
.expect("appsink")
|
|
|
|
|
.downcast::<gst_app::AppSink>()
|
|
|
|
|
.expect("appsink cast");
|
|
|
|
|
pipeline
|
|
|
|
|
.add_many([
|
|
|
|
|
src.upcast_ref::<gst::Element>(),
|
|
|
|
|
sink.upcast_ref::<gst::Element>(),
|
|
|
|
|
])
|
|
|
|
|
.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 };
|
|
|
|
|
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 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());
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|