use std::time::Duration;
use hiroz::{Builder, Result, context::ZContextBuilder, define_action, msg::ZSerializer};
use serde::{Deserialize, Serialize};
use tokio::time::timeout;
use zenoh::Wait;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TestGoal {
pub order: i32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TestResult {
pub value: i32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TestFeedback {
pub sequence: Vec<i32>,
}
pub struct TestAction;
define_action! {
TestAction,
action_name: "test_action_comm",
Goal: TestGoal,
Result: TestResult,
Feedback: TestFeedback,
}
async fn setup_test() -> Result<(
hiroz::context::ZContext,
hiroz::node::ZNode,
hiroz::action::client::ZActionClient<TestAction>,
hiroz::action::server::ZActionServer<TestAction>,
)> {
let ctx = ZContextBuilder::default().build()?;
let node = ctx.create_node("test_action_comm_node").build()?;
let server = node
.create_action_server::<TestAction>("test_action_comm")
.build()?;
let client = node
.create_action_client::<TestAction>("test_action_comm")
.build()?;
tokio::time::sleep(Duration::from_millis(500)).await;
Ok((ctx, node, client, server))
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_valid_goal_comm() -> Result<()> {
let (_ctx, _node, client, server) = setup_test().await?;
let server_clone = server.clone();
let server_task = tokio::spawn(async move {
let requested = timeout(Duration::from_secs(5), server_clone.recv_goal())
.await
.expect("timeout receiving goal")?;
let goal_order = requested.goal.order;
let goal_id = requested.info.goal_id;
let _accepted = requested.accept();
Ok::<_, zenoh::Error>((goal_order, goal_id))
});
let outgoing_goal = TestGoal { order: 10 };
let goal_handle = timeout(
Duration::from_secs(5),
client.send_goal(outgoing_goal.clone()),
)
.await
.expect("timeout sending goal")?;
let (goal_order, goal_id) = server_task.await.expect("server task failed")?;
assert_eq!(goal_order, outgoing_goal.order);
assert_eq!(goal_id, goal_handle.id());
assert_ne!(goal_handle.id(), hiroz::action::GoalId::default());
drop(server);
drop(client);
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_valid_cancel_comm() -> Result<()> {
let (_ctx, _node, client, server) = setup_test().await?;
let server_clone = server.clone();
let server_task = tokio::spawn(async move {
let requested = timeout(Duration::from_secs(5), server_clone.recv_goal())
.await
.expect("timeout receiving goal")?;
let goal_id = requested.info.goal_id;
let accepted = requested.accept();
let _executing = accepted.execute();
let (cancel_request, response_tx) =
timeout(Duration::from_secs(5), server_clone.recv_cancel())
.await
.expect("timeout receiving cancel")?;
assert_eq!(cancel_request.goal_info.goal_id, goal_id);
let cancel_resp = hiroz::action::messages::CancelGoalResponse {
return_code: 0, goals_canceling: vec![hiroz::action::GoalInfo {
goal_id,
stamp: hiroz::action::Time::zero(), }],
};
let response_bytes = hiroz::msg::SerdeCdrSerdes::<
hiroz::action::messages::CancelGoalResponse,
>::serialize(&cancel_resp);
response_tx
.reply(response_tx.key_expr().clone(), response_bytes)
.wait()?;
Ok::<_, zenoh::Error>(())
});
let goal = TestGoal { order: 10 };
let goal_handle = timeout(Duration::from_secs(5), client.send_goal(goal))
.await
.expect("timeout sending goal")?;
let cancel_response = timeout(Duration::from_secs(5), goal_handle.cancel())
.await
.expect("timeout sending cancel")?;
server_task.await.expect("server task failed")?;
assert_eq!(cancel_response.return_code, 0);
drop(server);
drop(client);
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_valid_result_comm() -> Result<()> {
let (_ctx, _node, client, server) = setup_test().await?;
let server_clone = server.clone();
let server_task = tokio::spawn(async move {
let requested = timeout(Duration::from_secs(5), server_clone.recv_goal())
.await
.expect("timeout receiving goal")?;
let accepted = requested.accept();
let executing = accepted.execute();
let outgoing_result = TestResult { value: 42 };
executing.succeed(outgoing_result.clone())?;
Ok::<_, zenoh::Error>(outgoing_result)
});
let goal = TestGoal { order: 10 };
let goal_handle = timeout(Duration::from_secs(5), client.send_goal(goal))
.await
.expect("timeout sending goal")?;
let outgoing_result = server_task.await.expect("server task failed")?;
let incoming_result = timeout(Duration::from_secs(5), goal_handle.result())
.await
.expect("timeout getting result")?;
assert_eq!(incoming_result.value, outgoing_result.value);
drop(server);
drop(client);
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_valid_feedback_comm() -> Result<()> {
let (_ctx, _node, client, server) = setup_test().await?;
let server_clone = server.clone();
let server_task = tokio::spawn(async move {
let requested = timeout(Duration::from_secs(5), server_clone.recv_goal())
.await
.expect("timeout receiving goal")?;
let accepted = requested.accept();
let executing = accepted.execute();
let outgoing_feedback = TestFeedback {
sequence: vec![0, 1, 1, 2, 3, 5, 8, 13],
};
executing.publish_feedback(outgoing_feedback.clone())?;
tokio::time::sleep(Duration::from_millis(100)).await;
executing.succeed(TestResult { value: 13 })?;
Ok::<_, zenoh::Error>(outgoing_feedback)
});
let goal = TestGoal { order: 10 };
let mut goal_handle = timeout(Duration::from_secs(5), client.send_goal(goal))
.await
.expect("timeout sending goal")?;
let mut feedback_rx = goal_handle
.feedback()
.expect("failed to get feedback stream");
let incoming_feedback = timeout(Duration::from_secs(5), feedback_rx.recv())
.await
.expect("timeout receiving feedback")
.expect("feedback channel closed");
let outgoing_feedback = server_task.await.expect("server task failed")?;
assert_eq!(incoming_feedback.sequence, outgoing_feedback.sequence);
drop(server);
drop(client);
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_valid_status_comm() -> Result<()> {
let (_ctx, _node, client, server) = setup_test().await?;
let server_clone = server.clone();
let server_task = tokio::spawn(async move {
let requested = timeout(Duration::from_secs(5), server_clone.recv_goal())
.await
.expect("timeout receiving goal")?;
let accepted = requested.accept();
tokio::time::sleep(Duration::from_millis(300)).await;
let executing = accepted.execute();
tokio::time::sleep(Duration::from_millis(300)).await;
executing.succeed(TestResult { value: 42 })?;
Ok::<_, zenoh::Error>(())
});
let goal = TestGoal { order: 10 };
let goal_handle = timeout(Duration::from_secs(5), client.send_goal(goal))
.await
.expect("timeout sending goal")?;
let mut status_watch = client
.status_watch(goal_handle.id())
.expect("failed to watch status");
let mut final_status = *status_watch.borrow();
let mut iterations = 0;
while final_status != hiroz::action::GoalStatus::Succeeded && iterations < 10 {
match timeout(Duration::from_millis(600), status_watch.changed()).await {
Ok(Ok(_)) => {
final_status = *status_watch.borrow();
}
Ok(Err(_)) => break, Err(_) => {
final_status = *status_watch.borrow();
break;
}
}
iterations += 1;
}
assert_eq!(
final_status,
hiroz::action::GoalStatus::Succeeded,
"Expected Succeeded status after {} iterations",
iterations
);
server_task.await.expect("server task failed")?;
drop(server);
drop(client);
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_try_process_cancel_multi_goal() -> Result<()> {
let (_ctx, _node, client, server) = setup_test().await?;
let goal1 = TestGoal { order: 1 };
let goal2 = TestGoal { order: 2 };
let server_clone = server.clone();
let server_task = tokio::spawn(async move {
let req1 = timeout(Duration::from_secs(5), server_clone.recv_goal())
.await
.expect("timeout receiving goal 1")?;
let handle1 = req1.accept().execute();
let req2 = timeout(Duration::from_secs(5), server_clone.recv_goal())
.await
.expect("timeout receiving goal 2")?;
let handle2 = req2.accept().execute();
Ok::<_, zenoh::Error>((handle1, handle2))
});
let goal_handle1 = timeout(Duration::from_secs(5), client.send_goal(goal1))
.await
.expect("timeout sending goal 1")?;
let goal_handle2 = timeout(Duration::from_secs(5), client.send_goal(goal2))
.await
.expect("timeout sending goal 2")?;
let (handle1, handle2) = server_task.await.expect("server task failed")?;
let client_task = tokio::spawn(async move {
let cancel_response = timeout(Duration::from_secs(5), goal_handle2.cancel())
.await
.expect("timeout awaiting cancel response")?;
let result = timeout(Duration::from_secs(5), goal_handle2.result())
.await
.expect("timeout getting result 2")?;
Ok::<_, zenoh::Error>((cancel_response, result))
});
tokio::time::sleep(Duration::from_millis(200)).await;
assert!(
!handle1.try_process_cancel(),
"handle1.try_process_cancel() should return false (cancel was for goal2)"
);
assert!(
handle2.try_process_cancel(),
"handle2.try_process_cancel() should return true (cancel was for goal2)"
);
handle1.succeed(TestResult { value: 1 })?;
handle2.canceled(TestResult { value: 2 })?;
let (cancel_response, _) = client_task.await.expect("client task panicked")?;
assert_eq!(cancel_response.return_code, 1);
let _ = timeout(Duration::from_secs(5), goal_handle1.result())
.await
.expect("timeout getting result 1")?;
drop(server);
drop(client);
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn test_multiple_goals_comm() -> Result<()> {
let (_ctx, _node, client, server) = setup_test().await?;
let server_clone = server.clone();
let server_task = tokio::spawn(async move {
for i in 0..3 {
let requested = timeout(Duration::from_secs(2), server_clone.recv_goal())
.await
.expect("timeout receiving goal")?;
assert_eq!(requested.goal.order, i * 10);
let accepted = requested.accept();
let executing = accepted.execute();
executing.succeed(TestResult { value: i * 100 })?;
}
Ok::<_, zenoh::Error>(())
});
let mut goal_handles = vec![];
for i in 0..3 {
let goal = TestGoal { order: i * 10 };
let handle = timeout(Duration::from_secs(2), client.send_goal(goal))
.await
.expect("timeout sending goal")?;
goal_handles.push(handle);
}
server_task.await.expect("server task failed")?;
for (i, handle) in goal_handles.into_iter().enumerate() {
let result = timeout(Duration::from_secs(2), handle.result())
.await
.expect("timeout getting result")?;
assert_eq!(result.value, i as i32 * 100);
}
drop(server);
drop(client);
tokio::time::sleep(Duration::from_millis(50)).await;
Ok(())
}
}