lesavka/testing/tests/server_audio_include_contract.rs

147 lines
5.1 KiB
Rust
Raw Normal View History

//! 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.
pub use lesavka_server::camera;
#[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");
}
}