#[cfg(all(test, feature = "std"))]
mod tests {
use cu29_runtime::config::{
NodeId, read_configuration, read_configuration_with_features,
read_configuration_with_resolved_ron_and_features,
};
use std::collections::BTreeSet;
use std::fs::{create_dir_all, write};
use tempfile::tempdir;
#[test]
fn test_basic_include() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let config_dir = base_path.join("config");
create_dir_all(&config_dir).unwrap();
let included_config = r#"(
tasks: [
(
id: "included_task",
type: "tasks::IncludedTask",
),
],
cnx: [],
)"#;
let included_path = config_dir.join("included.ron");
write(&included_path, included_config).unwrap();
let main_config = r#"(
tasks: [
(
id: "main_task",
type: "tasks::MainTask",
),
],
cnx: [],
includes: [
(
path: "included.ron",
params: {},
),
],
)"#
.to_string();
let main_path = config_dir.join("main.ron");
write(&main_path, main_config).unwrap();
let config = read_configuration(main_path.to_str().unwrap()).unwrap();
let graph = config.get_graph(None).unwrap();
let all_nodes = graph.get_all_nodes();
assert_eq!(all_nodes.len(), 2);
let task_ids: Vec<_> = all_nodes
.iter()
.map(|(_, node)| node.get_id().to_string())
.collect();
assert!(task_ids.contains(&"main_task".to_string()));
assert!(task_ids.contains(&"included_task".to_string()));
}
#[test]
fn test_constants_merge_from_includes_with_root_precedence() {
let temp_dir = tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
create_dir_all(&config_dir).unwrap();
write(
config_dir.join("included.ron"),
r#"(
constants: [
(
id: "ROOT_WINS",
module: "diagnostics::counters",
storage: usize,
value: 99,
),
(id: "ROOT_WINS", module: "telemetry", storage: usize, value: 11),
(id: "FROM_INCLUDE", quantity: length, unit: millimeter, value: 250.0),
(
id: "FROM_EXPRESSION",
module: "geometry",
type: "crate::Pair",
expression: "crate::Pair::new()",
),
],
tasks: [],
cnx: [],
)"#,
)
.unwrap();
let main_path = config_dir.join("main.ron");
write(
&main_path,
r#"(
constants: [
(
id: "ROOT_WINS",
module: "diagnostics :: counters",
storage: usize,
value: 7,
),
],
tasks: [],
cnx: [],
includes: [(path: "included.ron")],
)"#,
)
.unwrap();
let config = read_configuration(main_path.to_str().unwrap()).unwrap();
let constants = &config.constants;
assert_eq!(constants.len(), 4);
assert_eq!(
constants[0].qualified_id(),
"diagnostics::counters::ROOT_WINS"
);
assert_eq!(constants[0].numbers().unwrap().1[0].as_f64(), 7.0);
assert_eq!(constants[1].qualified_id(), "telemetry::ROOT_WINS");
assert_eq!(constants[1].numbers().unwrap().1[0].as_f64(), 11.0);
assert_eq!(constants[2].qualified_id(), "constants::FROM_INCLUDE");
assert_eq!(constants[2].normalized_f32().unwrap().1, vec![0.25]);
assert_eq!(constants[3].qualified_id(), "geometry::FROM_EXPRESSION");
assert_eq!(
constants[3].expression_definition(),
Some(("crate::Pair", "crate::Pair::new()"))
);
}
#[test]
fn test_feature_predicate_combinations() {
let temp_dir = tempdir().unwrap();
let config_dir = temp_dir.path().join("config");
create_dir_all(&config_dir).unwrap();
for (name, task_id) in [
("feature", "feature_task"),
("not", "not_task"),
("all", "all_task"),
("any", "any_task"),
] {
write(
config_dir.join(format!("{name}.ron")),
format!(
r#"(
tasks: [(id: "{task_id}", type: "tasks::Marker")],
cnx: [],
)"#
),
)
.unwrap();
}
let main_path = config_dir.join("main.ron");
write(
&main_path,
r#"(
tasks: [],
cnx: [],
includes: [
(path: "feature.ron", when: Feature("camera")),
(path: "not.ron", when: Not(Feature("camera"))),
(
path: "all.ron",
when: All([Feature("camera"), Feature("recording")]),
),
(
path: "any.ron",
when: Any([Feature("camera"), Feature("recording")]),
),
],
)"#,
)
.unwrap();
let cases: &[(&[&str], &[&str])] = &[
(&[], &["not_task"]),
(&["camera"], &["any_task", "feature_task"]),
(
&["camera", "recording"],
&["all_task", "any_task", "feature_task"],
),
(&["recording"], &["any_task", "not_task"]),
];
for (features, expected_tasks) in cases {
let config =
read_configuration_with_features(main_path.to_str().unwrap(), features).unwrap();
let actual: BTreeSet<_> = config
.get_graph(None)
.unwrap()
.get_all_nodes()
.into_iter()
.map(|(_, node)| node.get_id())
.collect();
let expected: BTreeSet<_> = expected_tasks
.iter()
.map(|task| (*task).to_string())
.collect();
assert_eq!(actual, expected, "features={features:?}");
}
}
#[test]
fn test_disabled_feature_include_is_not_read() {
let temp_dir = tempdir().unwrap();
let main_path = temp_dir.path().join("main.ron");
write(
&main_path,
r#"(
tasks: [],
cnx: [],
includes: [
(path: "target-only.ron", when: Feature("target-camera")),
],
)"#,
)
.unwrap();
read_configuration_with_features(main_path.to_str().unwrap(), &[])
.expect("a disabled include must not touch its file");
let error =
read_configuration_with_features(main_path.to_str().unwrap(), &["target-camera"])
.expect_err("an enabled include must be read");
assert!(error.to_string().contains("target-only.ron"));
}
#[test]
fn test_resolved_ron_contains_only_the_selected_graph() {
let temp_dir = tempdir().unwrap();
let camera_path = temp_dir.path().join("camera.ron");
write(
&camera_path,
r#"(
tasks: [
(id: "camera", type: "target_camera::Camera"),
(id: "sink", type: "tasks::FrameSink"),
],
cnx: [
(src: "camera", dst: "sink", msg: "target_camera::Frame"),
],
)"#,
)
.unwrap();
let main_path = temp_dir.path().join("main.ron");
write(
&main_path,
r#"(
tasks: [],
cnx: [],
includes: [
(path: "camera.ron", when: Feature("target-camera")),
],
)"#,
)
.unwrap();
let (without_camera, resolved_without_camera) =
read_configuration_with_resolved_ron_and_features(main_path.to_str().unwrap(), &[])
.unwrap();
assert_eq!(without_camera.get_graph(None).unwrap().node_count(), 0);
assert!(!resolved_without_camera.contains("target_camera"));
assert!(!resolved_without_camera.contains("camera.ron"));
assert!(!resolved_without_camera.contains("Feature"));
let (with_camera, resolved_with_camera) =
read_configuration_with_resolved_ron_and_features(
main_path.to_str().unwrap(),
&["target-camera"],
)
.unwrap();
let graph = with_camera.get_graph(None).unwrap();
assert_eq!(graph.node_count(), 2);
assert_eq!(graph.edge_count(), 1);
assert!(resolved_with_camera.contains("target_camera::Camera"));
assert!(resolved_with_camera.contains("target_camera::Frame"));
assert!(!resolved_with_camera.contains("camera.ron"));
assert!(!resolved_with_camera.contains("Feature"));
}
#[test]
fn test_parameter_substitution() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let config_dir = base_path.join("config");
create_dir_all(&config_dir).unwrap();
let included_config = r#"(
tasks: [
(
id: "task_{{instance_id}}",
type: "tasks::Task{{instance_id}}",
config: {
"param_value": {{param_value}},
},
),
],
cnx: [],
)"#;
let included_path = config_dir.join("included.ron");
write(&included_path, included_config).unwrap();
let main_config = r#"(
tasks: [],
cnx: [],
includes: [
(
path: "included.ron",
params: {
"instance_id": "42",
"param_value": 100,
},
),
],
)"#
.to_string();
let main_path = config_dir.join("main.ron");
write(&main_path, main_config).unwrap();
let config = read_configuration(main_path.to_str().unwrap()).unwrap();
let graph = config.get_graph(None).unwrap();
let all_nodes = graph.get_all_nodes();
assert_eq!(all_nodes.len(), 1);
let (_, node) = all_nodes[0];
assert_eq!(node.get_id(), "task_42");
assert_eq!(node.get_type(), "tasks::Task42");
assert_eq!(
node.get_param::<i32>("param_value")
.expect("param_value lookup failed"),
Some(100)
);
}
#[test]
fn test_nested_includes() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let config_dir = base_path.join("config");
create_dir_all(&config_dir).unwrap();
let nested_config = r#"(
tasks: [
(
id: "nested_task",
type: "tasks::NestedTask",
),
],
cnx: [],
)"#;
let nested_path = config_dir.join("nested.ron");
write(&nested_path, nested_config).unwrap();
let middle_config = r#"(
tasks: [
(
id: "middle_task",
type: "tasks::MiddleTask",
),
],
cnx: [],
includes: [
(
path: "nested.ron",
params: {},
),
],
)"#
.to_string();
let middle_path = config_dir.join("middle.ron");
write(&middle_path, middle_config).unwrap();
let main_config = r#"(
tasks: [
(
id: "main_task",
type: "tasks::MainTask",
),
],
cnx: [],
includes: [
(
path: "middle.ron",
params: {},
),
],
)"#
.to_string();
let main_path = config_dir.join("main.ron");
write(&main_path, main_config).unwrap();
let config = read_configuration(main_path.to_str().unwrap()).unwrap();
let graph = config.get_graph(None).unwrap();
let all_nodes = graph.get_all_nodes();
assert_eq!(all_nodes.len(), 3);
let task_ids: Vec<_> = all_nodes
.iter()
.map(|(_, node)| node.get_id().to_string())
.collect();
assert!(task_ids.contains(&"main_task".to_string()));
assert!(task_ids.contains(&"middle_task".to_string()));
assert!(task_ids.contains(&"nested_task".to_string()));
}
#[test]
fn test_override_behavior() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let config_dir = base_path.join("config");
create_dir_all(&config_dir).unwrap();
let included_config = r#"(
tasks: [
(
id: "common_task",
type: "tasks::IncludedTask",
config: {
"param": 10,
},
),
(
id: "only_included_task",
type: "tasks::OnlyIncludedTask",
),
],
cnx: [],
monitors: [(
type: "tasks::IncludedMonitor",
)],
)"#;
let included_path = config_dir.join("included.ron");
write(&included_path, included_config).unwrap();
let main_config = r#"(
tasks: [
(
id: "common_task",
type: "tasks::MainTask",
config: {
"param": 20,
},
),
(
id: "only_main_task",
type: "tasks::OnlyMainTask",
),
],
cnx: [],
monitors: [(
type: "tasks::MainMonitor",
)],
includes: [
(
path: "included.ron",
params: {},
),
],
)"#
.to_string();
let main_path = config_dir.join("main.ron");
write(&main_path, main_config).unwrap();
let config = read_configuration(main_path.to_str().unwrap()).unwrap();
let graph = config.get_graph(None).unwrap();
let all_nodes = graph.get_all_nodes();
assert_eq!(all_nodes.len(), 3);
let task_ids: Vec<_> = all_nodes
.iter()
.map(|(_, node)| node.get_id().to_string())
.collect();
assert!(task_ids.contains(&"common_task".to_string()));
assert!(task_ids.contains(&"only_included_task".to_string()));
assert!(task_ids.contains(&"only_main_task".to_string()));
let common_task = all_nodes
.iter()
.find(|(_, node)| node.get_id() == "common_task")
.unwrap()
.1;
assert_eq!(common_task.get_type(), "tasks::MainTask");
assert_eq!(
common_task
.get_param::<i32>("param")
.expect("param lookup failed"),
Some(20)
);
assert_eq!(
config.get_monitor_config().unwrap().get_type(),
"tasks::MainMonitor"
);
}
#[test]
fn test_error_handling() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let config_dir = base_path.join("config");
create_dir_all(&config_dir).unwrap();
let main_config = r#"(
tasks: [],
cnx: [],
includes: [
(
path: "non_existent.ron",
params: {},
),
],
)"#;
let main_path = config_dir.join("main.ron");
write(&main_path, main_config).unwrap();
let result = read_configuration(main_path.to_str().unwrap());
assert!(result.is_err());
let invalid_config = r#"(
tasks: (
(
id: "invalid_task",
type: "tasks::InvalidTask",
),
),
cnx: [],
)"#;
let invalid_path = config_dir.join("invalid.ron");
write(&invalid_path, invalid_config).unwrap();
let main_config = r#"(
tasks: [],
cnx: [],
includes: [
(
path: "invalid.ron",
params: {},
),
],
)"#
.to_string();
let main_path = config_dir.join("main.ron");
write(&main_path, main_config).unwrap();
let result = read_configuration(main_path.to_str().unwrap());
assert!(result.is_err());
}
#[test]
fn test_include_preserves_parallel_connections_with_different_messages() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let config_dir = base_path.join("config");
create_dir_all(&config_dir).unwrap();
let included_config = r#"(
tasks: [
(
id: "multi_src",
type: "tasks::MultiSource",
),
(
id: "multi_input",
type: "tasks::MultiInput",
),
],
cnx: [
(src: "multi_src", dst: "multi_input", msg: "i32"),
(src: "multi_src", dst: "multi_input", msg: "u32"),
],
)"#;
let included_path = config_dir.join("included.ron");
write(&included_path, included_config).unwrap();
let main_config = r#"(
tasks: [],
cnx: [],
includes: [
(
path: "included.ron",
params: {},
),
],
)"#;
let main_path = config_dir.join("main.ron");
write(&main_path, main_config).unwrap();
let config = read_configuration(main_path.to_str().unwrap()).unwrap();
let graph = config.get_graph(None).unwrap();
let mut messages: Vec<_> = graph
.edges()
.filter(|edge| edge.src == "multi_src" && edge.dst == "multi_input")
.map(|edge| edge.msg.as_str())
.collect();
messages.sort();
assert_eq!(messages, ["i32", "u32"]);
}
#[test]
fn test_multiple_parameterized_includes() {
let temp_dir = tempdir().unwrap();
let base_path = temp_dir.path();
let config_dir = base_path.join("config");
create_dir_all(&config_dir).unwrap();
let sensor_detect_config = r#"(
tasks: [
(
id: "camera{{id}}",
type: "cu_camera::Camera",
config: {
"port": "{{port}}",
},
),
(
id: "detect{{id}}",
type: "cu_detector::Detector",
config: {
"threshold": {{threshold}},
},
),
],
cnx: [
(src: "camera{{id}}", dst: "detect{{id}}", msg: "cu_camera::CameraPayload"),
],
)"#;
let sensor_detect_path = config_dir.join("sensor-detect.ron");
write(&sensor_detect_path, sensor_detect_config).unwrap();
let main_config = r#"(
tasks: [
(
id: "octopus",
type: "cu_octopus::octopus0",
),
],
includes: [
( path: "sensor-detect.ron", params: { "id": 0, "port": "/dev/video0", "threshold": 0.5, },),
( path: "sensor-detect.ron", params: { "id": 1, "port": "/dev/video1", "threshold": 0.1, },),
( path: "sensor-detect.ron", params: { "id": 2, "port": "/dev/video2", "threshold": 0.7, },),
],
cnx: [
(src: "detect0", dst: "octopus", msg: "cu_detect::DetectionPayload"),
(src: "detect1", dst: "octopus", msg: "cu_detect::DetectionPayload"),
(src: "detect2", dst: "octopus", msg: "cu_detect::DetectionPayload"),
],
monitors: [(
type: "cu_consolemon::CuConsoleMon",
)]
)"#;
let main_path = config_dir.join("main.ron");
write(&main_path, main_config).unwrap();
let config = read_configuration(main_path.to_str().unwrap()).unwrap();
let graph = config.get_graph(None).unwrap();
let all_nodes = graph.get_all_nodes();
assert_eq!(all_nodes.len(), 7);
let octopus_task = all_nodes
.iter()
.find(|(_, node)| node.get_id() == "octopus")
.map(|(_, node)| node);
assert!(octopus_task.is_some());
assert_eq!(octopus_task.unwrap().get_type(), "cu_octopus::octopus0");
for id in 0..3 {
let camera_id = format!("camera{id}");
let camera_task = all_nodes
.iter()
.find(|(_, node)| node.get_id() == camera_id)
.map(|(_, node)| node);
assert!(camera_task.is_some());
let camera = camera_task.unwrap();
assert_eq!(camera.get_type(), "cu_camera::Camera");
let expected_port = format!("/dev/video{id}");
let port = camera
.get_param::<String>("port")
.expect("port lookup failed")
.expect("missing port");
assert_eq!(port, expected_port);
}
let expected_thresholds = [0.5, 0.1, 0.7];
#[allow(clippy::needless_range_loop)]
for id in 0..3 {
let detect_id = format!("detect{id}");
let detect_task = all_nodes
.iter()
.find(|(_, node)| node.get_id() == detect_id)
.map(|(_, node)| node);
assert!(detect_task.is_some());
let detector = detect_task.unwrap();
assert_eq!(detector.get_type(), "cu_detector::Detector");
let threshold = detector
.get_param::<f64>("threshold")
.expect("threshold lookup failed")
.expect("missing threshold");
assert_eq!(threshold, expected_thresholds[id]);
}
let graph = config.graphs.get_graph(None).unwrap();
let mut camera_node_ids = Vec::new();
let mut detector_node_ids = Vec::new();
let mut octopus_node_id = None;
for idx in graph.node_indices() {
let node_index = idx.index() as u32;
let node = graph.get_node_weight(node_index).unwrap();
let id = node.get_id();
if id == "octopus" {
octopus_node_id = Some(idx);
} else if id.starts_with("camera") {
camera_node_ids.push((id.clone(), idx));
} else if id.starts_with("detect") {
detector_node_ids.push((id.clone(), idx));
}
}
for id in 0..3 {
let camera_id = format!("camera{id}");
let detect_id = format!("detect{id}");
let camera_idx = graph.get_node_id_by_name(camera_id.as_str()).unwrap();
let detector_idx = graph.get_node_id_by_name(detect_id.as_str()).unwrap();
let has_connection = graph
.get_connection_msg_type(camera_idx, detector_idx)
.unwrap();
assert_eq!(has_connection, "cu_camera::CameraPayload");
}
let octopus_idx = octopus_node_id.unwrap();
for id in 0..3 {
let detect_id = format!("detect{id}");
let detector_idx = detector_node_ids
.iter()
.find(|(id, _)| *id == detect_id)
.unwrap()
.1;
let has_connection = graph
.get_connection_msg_type(
detector_idx.index() as NodeId,
octopus_idx.index() as NodeId,
)
.unwrap();
assert_eq!(has_connection, "cu_detect::DetectionPayload");
}
assert!(config.get_monitor_config().is_some());
assert_eq!(
config.get_monitor_config().unwrap().get_type(),
"cu_consolemon::CuConsoleMon"
);
}
}