235 lines
7.9 KiB
Rust
235 lines
7.9 KiB
Rust
//! Include-based coverage for client audio output sink selection helpers.
|
|
//!
|
|
//! Scope: include `client/src/output/audio.rs` and exercise sink discovery
|
|
//! branches with controlled `pactl` fixtures.
|
|
//! Targets: `client/src/output/audio.rs`.
|
|
//! Why: keep sink-resolution behavior deterministic without requiring live
|
|
//! desktop audio devices in CI.
|
|
|
|
#[allow(warnings)]
|
|
mod audio_include_contract {
|
|
include!(env!("LESAVKA_CLIENT_OUTPUT_AUDIO_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 pick_sink_element_prefers_operator_override() {
|
|
with_var("LESAVKA_AUDIO_SINK", Some("fakesink sync=false"), || {
|
|
let sink = pick_sink_element().expect("override sink");
|
|
assert_eq!(sink, "fakesink sync=false");
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn pick_sink_element_wraps_bare_device_override_for_pulsesink() {
|
|
with_var(
|
|
"LESAVKA_AUDIO_SINK",
|
|
Some("alsa_output.pci-0000_00_1f.3.analog-stereo"),
|
|
|| {
|
|
let sink = pick_sink_element().expect("device sink");
|
|
assert_eq!(
|
|
sink,
|
|
"pulsesink device=\"alsa_output.pci-0000_00_1f.3.analog-stereo\" buffer-time=350000 latency-time=100000 sync=true"
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn pick_sink_element_uses_default_sink_from_pactl_info() {
|
|
let script = r#"#!/usr/bin/env sh
|
|
if [ "$1" = "info" ]; then
|
|
echo "Server String: /run/user/1000/pulse/native"
|
|
echo "Default Sink: alsa_output.usb-DAC_1234-00.analog-stereo"
|
|
exit 0
|
|
fi
|
|
exit 0
|
|
"#;
|
|
with_fake_pactl(script, || {
|
|
with_var("LESAVKA_AUDIO_SINK", None::<&str>, || {
|
|
let sinks = list_pw_sinks();
|
|
assert_eq!(
|
|
sinks,
|
|
vec![(
|
|
"alsa_output.usb-DAC_1234-00.analog-stereo".to_string(),
|
|
"DEFAULT".to_string()
|
|
)]
|
|
);
|
|
let sink = pick_sink_element().expect("pick sink");
|
|
assert_eq!(
|
|
sink,
|
|
"pulsesink device=\"alsa_output.usb-DAC_1234-00.analog-stereo\" buffer-time=350000 latency-time=100000 sync=true"
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn bluetooth_sink_override_uses_more_headroom() {
|
|
with_var(
|
|
"LESAVKA_AUDIO_SINK",
|
|
Some("bluez_output.80_C3_BA_76_26_AB.1"),
|
|
|| {
|
|
let sink = pick_sink_element().expect("bluetooth sink");
|
|
assert_eq!(
|
|
sink,
|
|
"pulsesink device=\"bluez_output.80_C3_BA_76_26_AB.1\" buffer-time=750000 latency-time=250000 sync=true"
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn pick_sink_element_falls_back_to_autoaudiosink_without_pactl_default() {
|
|
let script = r#"#!/usr/bin/env sh
|
|
if [ "$1" = "info" ]; then
|
|
echo "Server String: /run/user/1000/pulse/native"
|
|
echo "Default Source: alsa_input.usb-Mic_1234-00.analog-stereo"
|
|
exit 0
|
|
fi
|
|
exit 0
|
|
"#;
|
|
with_fake_pactl(script, || {
|
|
with_var("LESAVKA_AUDIO_SINK", None::<&str>, || {
|
|
assert!(
|
|
list_pw_sinks().is_empty(),
|
|
"no default sink should be parsed"
|
|
);
|
|
let sink = pick_sink_element().expect("fallback sink");
|
|
assert_eq!(sink, "autoaudiosink");
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn audio_out_new_and_push_are_stable_with_sink_override() {
|
|
with_var("LESAVKA_AUDIO_SINK", Some("fakesink sync=false"), || {
|
|
with_var("LESAVKA_AUDIO_GAIN", Some("2.5"), || {
|
|
with_var("LESAVKA_TAP_AUDIO", Some("1"), || match AudioOut::new() {
|
|
Ok(out) => {
|
|
out.push(AudioPacket {
|
|
id: 0,
|
|
pts: 1_234,
|
|
data: vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC],
|
|
});
|
|
drop(out);
|
|
}
|
|
Err(err) => {
|
|
assert!(!err.to_string().trim().is_empty());
|
|
}
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn audio_out_new_returns_error_for_invalid_sink_override() {
|
|
with_var(
|
|
"LESAVKA_AUDIO_SINK",
|
|
Some("definitely-not-a-real-gst-sink"),
|
|
|| {
|
|
with_var("LESAVKA_TAP_AUDIO", None::<&str>, || {
|
|
let result = AudioOut::new();
|
|
assert!(
|
|
result.is_err(),
|
|
"invalid sink override must fail pipeline parsing"
|
|
);
|
|
});
|
|
},
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn live_audio_buffer_leaves_pts_for_appsrc_timestamping() {
|
|
let _ = gst::init();
|
|
let timeline = std::sync::Mutex::new(AudioTimeline::default());
|
|
let buffer = live_audio_buffer(
|
|
AudioPacket {
|
|
id: 0,
|
|
pts: 42_666,
|
|
data: vec![0xFF, 0xF1, 0x50, 0x80, 0x00, 0x1F, 0xFC],
|
|
},
|
|
&timeline,
|
|
);
|
|
|
|
assert_eq!(buffer.pts(), None);
|
|
assert_eq!(timeline.lock().expect("timeline").packets, 1);
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn downstream_audio_pipeline_keeps_working_aac_transport_with_gain_stage() {
|
|
with_var("LESAVKA_AUDIO_GAIN", Some("3.25"), || {
|
|
assert_eq!(audio_gain_from_env(), 3.25);
|
|
});
|
|
let desc = audio_output_pipeline_desc("fakesink sync=false", 3.25, false);
|
|
assert!(desc.contains("aacparse ! avdec_aac"));
|
|
assert!(desc.contains("volume name=remote_audio_gain volume=3.250"));
|
|
assert!(desc.contains("level name=remote_audio_level"));
|
|
assert!(desc.contains("fakesink sync=false"));
|
|
}
|
|
|
|
#[test]
|
|
#[serial]
|
|
fn audio_gain_env_defaults_and_clamps_for_soft_remote_audio() {
|
|
with_var("LESAVKA_AUDIO_GAIN", None::<&str>, || {
|
|
assert_eq!(audio_gain_from_env(), 2.0);
|
|
});
|
|
with_var("LESAVKA_AUDIO_GAIN", Some("99"), || {
|
|
assert_eq!(audio_gain_from_env(), 8.0);
|
|
});
|
|
with_var("LESAVKA_AUDIO_GAIN", Some("-1"), || {
|
|
assert_eq!(audio_gain_from_env(), 0.0);
|
|
});
|
|
with_var("LESAVKA_AUDIO_GAIN", Some("nope"), || {
|
|
assert_eq!(audio_gain_from_env(), 2.0);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn audio_gain_control_reads_first_token_and_clamps() {
|
|
let dir = tempdir().expect("tempdir");
|
|
let path = dir.path().join("gain.control");
|
|
fs::write(&path, "4.500 nonce\n").expect("write gain");
|
|
assert_eq!(read_audio_gain_control(&path), Some(4.5));
|
|
|
|
fs::write(&path, "20.0 nonce\n").expect("write clamped gain");
|
|
assert_eq!(read_audio_gain_control(&path), Some(8.0));
|
|
|
|
fs::write(&path, "bad nonce\n").expect("write invalid gain");
|
|
assert_eq!(read_audio_gain_control(&path), None);
|
|
}
|
|
}
|