use std::time::Duration;
use camel_api::{Exchange, Message, RouteStatus, RuntimeCommand};
use camel_builder::{RouteBuilder, StepAccumulator};
use camel_core::LanguageRegistryError;
use camel_language_rhai::RhaiLanguage;
use camel_test::CamelTestContext;
use tower::ServiceExt;
fn test_rt() -> std::sync::Arc<dyn camel_component_api::RuntimeObservability> {
std::sync::Arc::new(camel_component_api::NoOpComponentContext)
}
fn ensure_rhai_registered(ctx: &mut camel_core::CamelContext) {
match ctx.register_language("rhai", Box::new(RhaiLanguage::new())) {
Ok(()) | Err(LanguageRegistryError::AlreadyRegistered { .. }) => {}
}
}
async fn send_to_direct_ignoring_error(
h: &CamelTestContext,
endpoint_uri: &str,
exchange: Exchange,
) {
let producer = {
let ctx = h.ctx().lock().await;
let producer_ctx = ctx.producer_context();
let registry = ctx.registry();
let component = registry
.get("direct")
.expect("direct component not registered");
let endpoint = component
.create_endpoint(endpoint_uri, &*ctx)
.expect("failed to create direct endpoint");
endpoint
.create_producer(test_rt(), &producer_ctx)
.expect("failed to create direct producer")
};
let _ = producer.oneshot(exchange).await;
}
async fn send_to_direct_until_delivered(
h: &CamelTestContext,
endpoint_uri: &str,
exchange: Exchange,
timeout: Duration,
) {
let deadline = tokio::time::Instant::now() + timeout;
loop {
let producer = {
let ctx = h.ctx().lock().await;
let producer_ctx = ctx.producer_context();
let registry = ctx.registry();
let component = registry
.get("direct")
.expect("direct component not registered");
let endpoint = component
.create_endpoint(endpoint_uri, &*ctx)
.expect("failed to create direct endpoint");
endpoint
.create_producer(test_rt(), &producer_ctx)
.expect("failed to create direct producer")
};
match producer.oneshot(exchange.clone()).await {
Ok(_) => return,
Err(_) if tokio::time::Instant::now() < deadline => {
tokio::time::sleep(Duration::from_millis(20)).await;
}
Err(e) => panic!("failed to send exchange to {endpoint_uri} within {timeout:?}: {e}"),
}
}
}
async fn route_status(h: &CamelTestContext, route_id: &str) -> Option<RouteStatus> {
let ctx = h.ctx().lock().await;
let s = ctx
.runtime_route_status(route_id)
.await
.expect("runtime route status query failed")?;
Some(match s.as_str() {
"Stopped" | "Registered" => RouteStatus::Stopped,
"Starting" => RouteStatus::Starting,
"Started" => RouteStatus::Started,
"Stopping" => RouteStatus::Stopping,
"Suspended" => RouteStatus::Suspended,
"Failed" => RouteStatus::Failed("failed".to_string()),
other => panic!("unknown route status: {other}"),
})
}
async fn stop_route(h: &CamelTestContext, route_id: &str) {
let runtime = {
let ctx = h.ctx().lock().await;
ctx.runtime()
};
runtime
.execute(RuntimeCommand::StopRoute {
route_id: route_id.to_string(),
command_id: format!("test:stop:{route_id}"),
causation_id: None,
})
.await
.expect("failed to stop route");
}
async fn start_route(h: &CamelTestContext, route_id: &str) {
let runtime = {
let ctx = h.ctx().lock().await;
ctx.runtime()
};
runtime
.execute(RuntimeCommand::StartRoute {
route_id: route_id.to_string(),
command_id: format!("test:start:{route_id}"),
causation_id: None,
})
.await
.expect("failed to start route");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn sequential_multicast_top_level_direct_completes() {
let h = CamelTestContext::builder()
.with_timer()
.with_direct()
.with_mock()
.build()
.await;
let yaml = r#"
routes:
- id: "seq-multicast-producer"
from: "timer:seq?period=10&repeatCount=1"
startup_order: 200
steps:
- multicast:
aggregation: original
steps:
- to: "direct:seq-echo"
- id: "seq-multicast-echo"
from: "direct:seq-echo"
startup_order: 50
steps:
- to: "mock:seq-multicast-result"
"#;
for route in camel_dsl::parse_yaml(yaml).unwrap() {
h.add_route(route).await.unwrap();
}
h.start().await;
h.mock()
.get_endpoint("seq-multicast-result")
.expect("mock endpoint created during route compilation")
.await_exchanges(1, Duration::from_secs(2))
.await;
h.stop().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn top_level_direct_consumer_restart_reregisters() {
let h = CamelTestContext::builder()
.with_direct()
.with_mock()
.build()
.await;
let route = RouteBuilder::from("direct:restart-echo")
.route_id("restart-echo")
.to("mock:restart-result")
.build()
.unwrap();
h.add_route(route).await.unwrap();
h.start().await;
let mock = h
.mock()
.get_endpoint("restart-result")
.expect("mock endpoint created during route compilation");
send_to_direct_until_delivered(
&h,
"direct:restart-echo",
Exchange::new(Message::new("first")),
Duration::from_secs(2),
)
.await;
mock.await_exchanges(1, Duration::from_secs(2)).await;
assert_eq!(
route_status(&h, "restart-echo").await,
Some(RouteStatus::Started),
"consumer route should be Started before restart"
);
stop_route(&h, "restart-echo").await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
route_status(&h, "restart-echo").await,
Some(RouteStatus::Stopped),
"consumer route should be Stopped after stop_route"
);
send_to_direct_ignoring_error(
&h,
"direct:restart-echo",
Exchange::new(Message::new("while-stopped")),
)
.await;
let count_after_stop = mock.get_received_exchanges().await.len();
assert_eq!(
count_after_stop, 1,
"no exchange may be delivered while the consumer is stopped"
);
start_route(&h, "restart-echo").await;
tokio::time::sleep(Duration::from_millis(100)).await;
assert_eq!(
route_status(&h, "restart-echo").await,
Some(RouteStatus::Started),
"consumer route should be Started after start_route"
);
send_to_direct_until_delivered(
&h,
"direct:restart-echo",
Exchange::new(Message::new("after-restart")),
Duration::from_secs(2),
)
.await;
mock.await_exchanges(2, Duration::from_secs(2)).await;
h.stop().await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn top_level_direct_propagates_consumer_error() {
let h = CamelTestContext::builder()
.with_timer()
.with_direct()
.with_mock()
.build()
.await;
{
let mut guard = h.ctx().lock().await;
ensure_rhai_registered(&mut guard);
}
let producer = RouteBuilder::from("timer:err?period=10&repeatCount=1")
.route_id("err-producer")
.startup_order(200)
.to("direct:err-echo")
.to("mock:producer-after")
.build()
.unwrap();
let consumer = RouteBuilder::from("direct:err-echo")
.route_id("err-echo")
.startup_order(50)
.script("rhai", r#"throw "boom""#)
.to("mock:err-never")
.build()
.unwrap();
h.add_route(producer).await.unwrap();
h.add_route(consumer).await.unwrap();
h.start().await;
tokio::time::sleep(Duration::from_millis(300)).await;
h.stop().await;
let consumer_reached = h
.mock()
.get_endpoint("err-never")
.expect("mock endpoint created during route compilation")
.get_received_exchanges()
.await;
assert_eq!(
consumer_reached.len(),
0,
"consumer mock must not receive exchanges when its script throws"
);
let producer_continued = h
.mock()
.get_endpoint("producer-after")
.expect("mock endpoint created during route compilation")
.get_received_exchanges()
.await;
assert_eq!(
producer_continued.len(),
0,
"producer step after the failing `to: direct:` must be skipped (b′ propagation)"
);
}