Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 125 additions & 0 deletions crates/core/src/control.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,128 @@ pub enum EngineControlMessage {
},
Shutdown,
}

#[cfg(test)]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: PR is test-only and leaves production paths unchanged

All hunks in this PR add #[cfg(test)] modules below existing production definitions; no non-test function, struct, enum, or constant bodies were changed. That means the existing production behaviors for control serialization, config parsing, batching, output sending, pin matching, registry lookup/resource loading, metric boundaries, and view-data emission are unaffected by this PR.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

mod tests {
use super::*;

#[test]
fn node_control_update_params_serialization_roundtrip() {
let msg = NodeControlMessage::UpdateParams(serde_json::json!({"gain": 0.5}));
let json = serde_json::to_string(&msg).unwrap();
let deserialized: NodeControlMessage = serde_json::from_str(&json).unwrap();
match deserialized {
NodeControlMessage::UpdateParams(v) => {
assert_eq!(v["gain"], 0.5);
},
_ => panic!("expected UpdateParams"),
}
}

#[test]
fn node_control_start_serialization_roundtrip() {
let msg = NodeControlMessage::Start;
let json = serde_json::to_string(&msg).unwrap();
let deserialized: NodeControlMessage = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, NodeControlMessage::Start));
}

#[test]
fn node_control_shutdown_serialization_roundtrip() {
let msg = NodeControlMessage::Shutdown;
let json = serde_json::to_string(&msg).unwrap();
let deserialized: NodeControlMessage = serde_json::from_str(&json).unwrap();
assert!(matches!(deserialized, NodeControlMessage::Shutdown));
}

#[test]
fn connection_mode_default_is_reliable() {
assert_eq!(ConnectionMode::default(), ConnectionMode::Reliable);
}

#[test]
fn connection_mode_serialization_roundtrip() {
for mode in [ConnectionMode::Reliable, ConnectionMode::BestEffort] {
let json = serde_json::to_string(&mode).unwrap();
let deserialized: ConnectionMode = serde_json::from_str(&json).unwrap();
assert_eq!(mode, deserialized);
}
}

#[test]
fn connection_mode_serde_uses_snake_case() {
let json = serde_json::to_string(&ConnectionMode::BestEffort).unwrap();
assert_eq!(json, "\"best_effort\"");
}

#[test]
fn engine_control_add_node_debug_snapshot() {
let msg = EngineControlMessage::AddNode {
node_id: "node1".into(),
kind: "gain".into(),
params: Some(serde_json::json!({"gain": 1.0})),
};
let dbg = format!("{msg:?}");
assert!(dbg.contains("AddNode"), "Debug must name the variant");
assert!(dbg.contains("node1"));
assert!(dbg.contains("gain"));
}

#[test]
fn engine_control_remove_node_debug_snapshot() {
let msg = EngineControlMessage::RemoveNode { node_id: "node1".into() };
let dbg = format!("{msg:?}");
assert!(dbg.contains("RemoveNode"));
assert!(dbg.contains("node1"));
}

#[test]
fn engine_control_connect_debug_snapshot() {
let msg = EngineControlMessage::Connect {
from_node: "src".into(),
from_pin: "audio_out".into(),
to_node: "dst".into(),
to_pin: "audio_in".into(),
mode: ConnectionMode::BestEffort,
};
let dbg = format!("{msg:?}");
assert!(dbg.contains("Connect"));
assert!(dbg.contains("src"));
assert!(dbg.contains("audio_out"));
assert!(dbg.contains("dst"));
assert!(dbg.contains("audio_in"));
assert!(dbg.contains("BestEffort"));
}

#[test]
fn engine_control_disconnect_debug_snapshot() {
let msg = EngineControlMessage::Disconnect {
from_node: "src".into(),
from_pin: "out".into(),
to_node: "dst".into(),
to_pin: "in".into(),
};
let dbg = format!("{msg:?}");
assert!(dbg.contains("Disconnect"));
assert!(dbg.contains("src"));
assert!(dbg.contains("dst"));
}

#[test]
fn engine_control_tune_node_debug_snapshot() {
let msg = EngineControlMessage::TuneNode {
node_id: "node1".into(),
message: NodeControlMessage::UpdateParams(serde_json::json!({"rate": 44100})),
};
let dbg = format!("{msg:?}");
assert!(dbg.contains("TuneNode"));
assert!(dbg.contains("node1"));
assert!(dbg.contains("UpdateParams"));
}

#[test]
fn engine_control_shutdown_debug_snapshot() {
let msg = EngineControlMessage::Shutdown;
assert_eq!(format!("{msg:?}"), "Shutdown");
}
}
127 changes: 127 additions & 0 deletions crates/core/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,130 @@ pub mod packet_helpers {
batch
}
}

#[cfg(test)]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Comment-heavy tests may conflict with the repository’s preference but are not a correctness issue

The repository guidance discourages comments that restate obvious code, and several touched files already contain substantial API documentation and the new tests add descriptive names rather than new production comments. I did not report this as a bug because comments/documentation style issues are not severe and the added test code mostly relies on test names for intent; any cleanup would be editorial rather than behavioral.

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

mod tests {
use super::*;
use serde::Deserialize;

#[derive(Debug, Deserialize, Default, PartialEq)]
struct TestConfig {
#[serde(default)]
gain: f32,
#[serde(default)]
channels: u16,
}

#[test]
fn parse_config_optional_with_valid_json() {
let params = serde_json::json!({"gain": 0.5, "channels": 2});
let cfg: TestConfig = config_helpers::parse_config_optional(Some(&params)).unwrap();
assert_eq!(cfg.gain, 0.5);
assert_eq!(cfg.channels, 2);
}

#[test]
fn parse_config_optional_with_none_returns_default() {
let cfg: TestConfig = config_helpers::parse_config_optional(None).unwrap();
assert_eq!(cfg, TestConfig::default());
}

#[test]
fn parse_config_optional_with_partial_json_fills_defaults() {
let params = serde_json::json!({"gain": 1.5});
let cfg: TestConfig = config_helpers::parse_config_optional(Some(&params)).unwrap();
assert_eq!(cfg.gain, 1.5);
assert_eq!(cfg.channels, 0);
}

#[test]
fn parse_config_required_with_valid_json() {
let params = serde_json::json!({"gain": 2.0, "channels": 1});
let cfg: TestConfig = config_helpers::parse_config_required(Some(&params)).unwrap();
assert_eq!(cfg.gain, 2.0);
assert_eq!(cfg.channels, 1);
}

#[test]
fn parse_config_required_with_none_returns_error() {
let result = config_helpers::parse_config_required::<TestConfig>(None);
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(err_str.contains("Configuration"), "expected Configuration error, got: {err_str}");
}

#[test]
fn parse_config_required_with_invalid_type_returns_error() {
let params = serde_json::json!({"gain": "not_a_number"});
let result = config_helpers::parse_config_required::<TestConfig>(Some(&params));
assert!(result.is_err());
}

#[test]
fn parse_config_with_context_missing_params() {
let result = config_helpers::parse_config_with_context::<TestConfig>(None, "AudioGain");
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(err_str.contains("AudioGain"));
}

#[test]
fn parse_config_with_context_invalid_json() {
let params = serde_json::json!("just a string");
let result =
config_helpers::parse_config_with_context::<TestConfig>(Some(&params), "AudioGain");
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(err_str.contains("AudioGain"));
}

#[test]
fn parse_config_optional_with_invalid_type_falls_back_to_default() {
let params = serde_json::json!({"gain": "not_a_number"});
let cfg: TestConfig = config_helpers::parse_config_optional(Some(&params)).unwrap();
assert_eq!(cfg, TestConfig::default());
}

#[test]
fn parse_config_with_context_valid_json() {
let params = serde_json::json!({"gain": 3.0, "channels": 4});
let cfg: TestConfig =
config_helpers::parse_config_with_context(Some(&params), "AudioGain").unwrap();
assert_eq!(cfg.gain, 3.0);
assert_eq!(cfg.channels, 4);
}

#[test]
fn batch_packets_greedy_drains_one_extra_packet() {
let (tx, mut rx) = tokio::sync::mpsc::channel(16);
let first = Packet::Text(std::sync::Arc::from("hello"));
tx.try_send(Packet::Text(std::sync::Arc::from("world"))).unwrap();
let batch = packet_helpers::batch_packets_greedy(first, &mut rx, 4);
assert_eq!(batch.len(), 2);
}

#[test]
fn batch_packets_greedy_empty_channel() {
let (_tx, mut rx) = tokio::sync::mpsc::channel::<Packet>(16);
let first = Packet::Text(std::sync::Arc::from("only"));
let batch = packet_helpers::batch_packets_greedy(first, &mut rx, 8);
assert_eq!(batch.len(), 1);
}

#[test]
fn batch_packets_greedy_respects_batch_size() {
let (tx, mut rx) = tokio::sync::mpsc::channel(16);
for i in 0..10 {
tx.try_send(Packet::Text(std::sync::Arc::from(format!("{i}")))).unwrap();
}
let first = Packet::Text(std::sync::Arc::from("first"));
let batch = packet_helpers::batch_packets_greedy(first, &mut rx, 3);
assert_eq!(batch.len(), 3);
}

#[test]
fn default_batch_capacity_is_reasonable() {
const { assert!(packet_helpers::DEFAULT_BATCH_CAPACITY >= 8) };
const { assert!(packet_helpers::DEFAULT_BATCH_CAPACITY <= 128) };
}
}
55 changes: 55 additions & 0 deletions crates/core/src/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,58 @@ pub const HISTOGRAM_BOUNDARIES_SESSION_DURATION: &[f64] =
/// minor jitter from severe overruns that cause A/V desync.
pub const HISTOGRAM_BOUNDARIES_FRAME_OVERRUN: &[f64] =
&[0.001, 0.005, 0.01, 0.02, 0.033, 0.05, 0.1, 0.5, 1.0];

#[cfg(test)]
mod tests {
use super::*;

fn assert_sorted_and_positive(name: &str, boundaries: &[f64]) {
assert!(!boundaries.is_empty(), "{name} must not be empty");
assert!(
boundaries[0] > 0.0,
"{name} first boundary must be positive, got {}",
boundaries[0]
);
for window in boundaries.windows(2) {
assert!(
window[0] < window[1],
"{name} boundaries must be strictly ascending: {} >= {}",
window[0],
window[1]
);
}
}

#[test]
fn all_boundary_arrays_sorted_and_positive() {
let arrays: &[(&str, &[f64])] = &[
("CODEC_PACKET", HISTOGRAM_BOUNDARIES_CODEC_PACKET),
("FILE_OPERATION", HISTOGRAM_BOUNDARIES_FILE_OPERATION),
("NODE_EXECUTION", HISTOGRAM_BOUNDARIES_NODE_EXECUTION),
("BACKPRESSURE", HISTOGRAM_BOUNDARIES_BACKPRESSURE),
("PACER_LATENESS", HISTOGRAM_BOUNDARIES_PACER_LATENESS),
("CLOCK_OFFSET_MS", HISTOGRAM_BOUNDARIES_CLOCK_OFFSET_MS),
("FRAME_GAP_MS", HISTOGRAM_BOUNDARIES_FRAME_GAP_MS),
("PIPELINE_DURATION", HISTOGRAM_BOUNDARIES_PIPELINE_DURATION),
("HTTP_DURATION", HISTOGRAM_BOUNDARIES_HTTP_DURATION),
("SESSION_DURATION", HISTOGRAM_BOUNDARIES_SESSION_DURATION),
("FRAME_OVERRUN", HISTOGRAM_BOUNDARIES_FRAME_OVERRUN),
];
for (name, arr) in arrays {
assert_sorted_and_positive(name, arr);
}
}

#[test]
fn codec_packet_covers_sub_millisecond_to_second() {
let b = HISTOGRAM_BOUNDARIES_CODEC_PACKET;
assert!(b[0] <= 0.0001, "should start at sub-millisecond range");
assert!(*b.last().unwrap() >= 1.0, "should reach at least 1 second");
}

#[test]
fn session_duration_covers_hours() {
let b = HISTOGRAM_BOUNDARIES_SESSION_DURATION;
assert!(*b.last().unwrap() >= 86400.0, "should cover up to 24 hours");
}
}
Loading
Loading