diff --git a/src/audio/pw_cli.rs b/src/audio/pw_cli.rs index 725856b..51d3ed6 100644 --- a/src/audio/pw_cli.rs +++ b/src/audio/pw_cli.rs @@ -70,3 +70,104 @@ fn parse_pw_nodes(text: &str) -> Vec { devices.sort_by(|a, b| a.description.cmp(&b.description)); devices } + +#[cfg(test)] +mod tests { + use super::*; + + const SAMPLE_NODES: &str = r#" + id 33, type PipeWire:Interface:Node/3 + node.name = "alsa_output.builtin" + node.description = "Built-in Speakers" + media.class = "Audio/Sink" + id 34, type PipeWire:Interface:Node/3 + node.name = "alsa_input.builtin" + node.description = "Built-in Mic" + media.class = "Audio/Source" + id 35, type PipeWire:Interface:Node/3 + node.name = "v4l2.cam" + media.class = "Video/Source" + id 36, type PipeWire:Interface:Node/3 + node.name = "bare.sink" + media.class = "Audio/Sink" + "#; + + #[test] + fn parses_audio_nodes_sorted_by_description() { + let devices = parse_pw_nodes(SAMPLE_NODES); + + // exactly 3 devices (the Video/Source node is dropped) + assert_eq!(devices.len(), 3); + + // order is by description: ["Built-in Mic", "Built-in Speakers", "bare.sink"] + // (uppercase "B" sorts before lowercase "b") + assert_eq!(devices[0].description, "Built-in Mic"); + assert_eq!(devices[1].description, "Built-in Speakers"); + assert_eq!(devices[2].description, "bare.sink"); + + // the mic device equals: + assert_eq!( + devices[0], + AudioDevice { + name: "alsa_input.builtin".into(), + description: "Built-in Mic".into(), + is_input: true, + } + ); + } + + #[test] + fn source_is_input_sink_is_output() { + let devices = parse_pw_nodes(SAMPLE_NODES); + // Find devices by name or description to verify is_input + let mic = devices.iter().find(|d| d.name == "alsa_input.builtin").unwrap(); + let speakers = devices.iter().find(|d| d.name == "alsa_output.builtin").unwrap(); + let bare = devices.iter().find(|d| d.name == "bare.sink").unwrap(); + + assert!(mic.is_input); + assert!(!speakers.is_input); + assert!(!bare.is_input); + } + + #[test] + fn description_falls_back_to_node_name() { + let devices = parse_pw_nodes(SAMPLE_NODES); + let bare = devices.iter().find(|d| d.name == "bare.sink").unwrap(); + assert_eq!(bare.description, "bare.sink"); + } + + #[test] + fn non_audio_and_empty_inputs_yield_nothing() { + assert!(parse_pw_nodes("").is_empty()); + + let video_sample = r#" + id 35, type PipeWire:Interface:Node/3 + node.name = "v4l2.cam" + media.class = "Video/Source" + "#; + assert!(parse_pw_nodes(video_sample).is_empty()); + } + + #[test] + fn final_block_is_emitted_at_eof() { + let single_sample = r#" + id 99, type PipeWire:Interface:Node/3 + node.name = "single.device" + media.class = "Audio/Sink" + "#; + let devices = parse_pw_nodes(single_sample); + assert_eq!(devices.len(), 1); + assert_eq!(devices[0].name, "single.device"); + } + + #[test] + fn incomplete_block_without_class_is_dropped() { + let incomplete_sample = r#" + id 100, type PipeWire:Interface:Node/3 + node.name = "incomplete.device" + node.description = "Incomplete Device" + "#; + let devices = parse_pw_nodes(incomplete_sample); + assert!(devices.is_empty()); + } +}