1use std::sync::{
4 Arc,
5 atomic::{AtomicBool, Ordering},
6};
7
8use camel_api::CamelError;
9use camel_component_direct::DirectComponent;
10use camel_component_log::LogComponent;
11use camel_component_mock::MockComponent;
12use camel_component_timer::TimerComponent;
13use camel_core::CamelContext;
14use camel_core::route::RouteDefinition;
15use tokio::sync::Mutex;
16
17use crate::time::TimeController;
18
19pub struct NoTimeControl;
24pub struct WithTimeControl;
25
26type Registration = Box<dyn FnOnce(&mut CamelContext) + Send>;
31
32pub struct CamelTestContextBuilder<S = NoTimeControl> {
36 registrations: Vec<Registration>,
37 mock: MockComponent,
38 _state: std::marker::PhantomData<S>,
39}
40
41impl CamelTestContextBuilder<NoTimeControl> {
42 pub(crate) fn new() -> Self {
43 Self {
44 registrations: Vec::new(),
45 mock: MockComponent::new(),
46 _state: std::marker::PhantomData,
47 }
48 }
49
50 pub fn with_time_control(self) -> CamelTestContextBuilder<WithTimeControl> {
53 CamelTestContextBuilder {
54 registrations: self.registrations,
55 mock: self.mock,
56 _state: std::marker::PhantomData,
57 }
58 }
59
60 pub async fn build(self) -> CamelTestContext {
62 build_context(self.registrations, self.mock).await
63 }
64}
65
66impl CamelTestContextBuilder<WithTimeControl> {
67 pub async fn build(self) -> (CamelTestContext, TimeController) {
72 tokio::time::pause();
73 let ctx = build_context(self.registrations, self.mock).await;
74 (ctx, TimeController)
75 }
76}
77
78macro_rules! impl_builder_methods {
79 ($S:ty) => {
80 impl CamelTestContextBuilder<$S> {
81 pub fn with_mock(self) -> Self {
84 self
85 }
86
87 pub fn with_mock_fail_fast(mut self) -> Self {
95 self.mock = camel_component_mock::MockComponent::with_config(
96 camel_component_mock::MockConfig {
97 fail_fast: true,
98 ..Default::default()
99 },
100 );
101 self
102 }
103
104 pub fn with_timer(mut self) -> Self {
106 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
107 ctx.register_component(TimerComponent::new());
108 }));
109 self
110 }
111
112 pub fn with_log(mut self) -> Self {
114 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
115 ctx.register_component(LogComponent::new());
116 }));
117 self
118 }
119
120 pub fn with_direct(mut self) -> Self {
122 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
123 ctx.register_component(DirectComponent::new());
124 }));
125 self
126 }
127
128 pub fn with_seda(mut self) -> Self {
130 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
131 ctx.register_component(camel_component_seda::SedaComponent::new());
132 }));
133 self
134 }
135
136 pub fn with_component<C>(mut self, component: C) -> Self
138 where
139 C: camel_component_api::Component + 'static,
140 {
141 self.registrations
142 .push(Box::new(move |ctx: &mut CamelContext| {
143 ctx.register_component(component);
144 }));
145 self
146 }
147 }
148 };
149}
150
151impl_builder_methods!(NoTimeControl);
152impl_builder_methods!(WithTimeControl);
153
154async fn build_context(registrations: Vec<Registration>, mock: MockComponent) -> CamelTestContext {
159 let mut ctx = CamelContext::builder().build().await.unwrap(); ctx.register_component(mock.clone());
163
164 for register in registrations {
166 register(&mut ctx);
167 }
168
169 let ctx = Arc::new(Mutex::new(ctx));
170 let stopped = Arc::new(AtomicBool::new(false));
171
172 CamelTestContext {
173 ctx: ctx.clone(),
174 mock,
175 stopped: stopped.clone(),
176 _guard: TestGuard { ctx, stopped },
177 }
178}
179
180pub(crate) struct TestGuard {
185 ctx: Arc<Mutex<CamelContext>>,
186 stopped: Arc<AtomicBool>,
187}
188
189impl Drop for TestGuard {
190 fn drop(&mut self) {
191 if self.stopped.swap(true, Ordering::SeqCst) {
192 return;
194 }
195 let ctx = self.ctx.clone();
196 if let Ok(handle) = tokio::runtime::Handle::try_current() {
197 match handle.runtime_flavor() {
198 tokio::runtime::RuntimeFlavor::MultiThread => {
199 tokio::task::block_in_place(|| {
201 handle.block_on(async move {
202 let mut ctx = ctx.lock().await;
203 let _ = ctx.stop().await;
204 });
205 });
206 }
207 tokio::runtime::RuntimeFlavor::CurrentThread => {
208 handle.spawn(async move {
211 let mut ctx = ctx.lock().await;
212 let _ = ctx.stop().await;
213 });
214 }
215 _ => {
216 handle.spawn(async move {
217 let mut ctx = ctx.lock().await;
218 let _ = ctx.stop().await;
219 });
220 }
221 }
222 }
223 }
224}
225
226pub struct CamelTestContext {
252 ctx: Arc<Mutex<CamelContext>>,
253 mock: MockComponent,
254 stopped: Arc<AtomicBool>,
255 _guard: TestGuard,
256}
257
258impl CamelTestContext {
259 pub fn builder() -> CamelTestContextBuilder<NoTimeControl> {
261 CamelTestContextBuilder::new()
262 }
263
264 pub async fn add_route(&self, route: RouteDefinition) -> Result<(), CamelError> {
266 let ctx = self.ctx.lock().await;
267 ctx.add_route_definition(route).await
268 }
269
270 pub async fn start(&self) {
272 let mut ctx = self.ctx.lock().await;
273 ctx.start().await.expect("CamelTestContext: start failed"); }
275
276 pub async fn stop(&self) {
279 if self.stopped.swap(true, Ordering::SeqCst) {
280 return; }
282 let mut ctx = self.ctx.lock().await;
283 ctx.stop().await.expect("CamelTestContext: stop failed"); }
285
286 pub async fn shutdown(self) {
288 self.stop().await;
289 }
290
291 pub fn mock(&self) -> &MockComponent {
293 &self.mock
294 }
295
296 pub fn ctx(&self) -> &Arc<Mutex<CamelContext>> {
298 &self.ctx
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 use camel_api::SpanKindHint;
306 use camel_builder::{RouteBuilder, StepAccumulator};
307 use std::time::Duration;
308
309 #[tokio::test]
310 async fn builder_without_time_control_builds_context() {
311 let harness = CamelTestContext::builder()
312 .with_mock()
313 .with_timer()
314 .with_log()
315 .build()
316 .await;
317
318 assert!(harness.mock().get_endpoint("result").is_none());
319 let guard = harness.ctx().lock().await;
320 let _ = &*guard;
321 }
322
323 #[tokio::test]
324 async fn builder_with_time_control_builds_and_advances_clock() {
325 let (_harness, time) = CamelTestContext::builder()
326 .with_mock()
327 .with_timer()
328 .with_time_control()
329 .build()
330 .await;
331
332 time.advance(Duration::from_millis(1)).await;
333 time.resume();
334 }
335
336 #[tokio::test]
337 async fn stop_is_idempotent_and_shutdown_is_safe() {
338 let harness = CamelTestContext::builder().with_mock().build().await;
339 harness.stop().await;
340 harness.stop().await;
341 harness.shutdown().await;
342 }
343
344 #[tokio::test]
345 async fn add_route_returns_error_for_invalid_step_uri() {
346 let harness = CamelTestContext::builder().with_mock().build().await;
347
348 let route = RouteBuilder::from("direct:start")
349 .route_id("bad-route")
350 .to("not-a-uri")
351 .build()
352 .unwrap();
353
354 let err = harness.add_route(route).await.expect_err("must fail");
355 assert!(err.to_string().contains("Invalid") || err.to_string().contains("invalid"));
356 }
357
358 #[tokio::test]
359 async fn with_component_registers_custom_component() {
360 let harness = CamelTestContext::builder()
361 .with_component(camel_component_direct::DirectComponent::new())
362 .with_mock()
363 .build()
364 .await;
365
366 let route = RouteBuilder::from("direct:start")
367 .route_id("direct-route")
368 .to("mock:out")
369 .build()
370 .unwrap();
371
372 harness.add_route(route).await.unwrap();
373 harness.start().await;
374 harness.stop().await;
375
376 let _guard = harness.ctx().lock().await;
378 }
379
380 #[tokio::test]
383 async fn tst004_route_lifecycle_start_stop_restart() {
384 let harness = CamelTestContext::builder()
385 .with_direct()
386 .with_mock()
387 .build()
388 .await;
389
390 let route = RouteBuilder::from("direct:lifecycle")
391 .route_id("lifecycle-route")
392 .to("mock:lifecycle-out")
393 .build()
394 .unwrap();
395
396 harness.add_route(route).await.unwrap();
397
398 harness.start().await;
400
401 harness.stop().await;
403
404 {
406 let mut ctx = harness.ctx().lock().await;
407 ctx.start().await.expect("restart should succeed");
408 ctx.stop().await.expect("stop after restart should succeed");
409 }
410 }
411
412 #[tokio::test]
415 async fn tst005_concurrent_exchange_processing() {
416 use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, Message};
417 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
418 use std::sync::Arc;
419 use std::sync::atomic::{AtomicU32, Ordering};
420 use tower::ServiceExt;
421
422 let counter = Arc::new(AtomicU32::new(0));
423 let processor: BoxProcessor = {
424 let c = Arc::clone(&counter);
425 BoxProcessor::from_fn(move |ex: Exchange| {
426 let c = Arc::clone(&c);
427 Box::pin(async move {
428 c.fetch_add(1, Ordering::Relaxed);
429 tokio::task::yield_now().await;
430 Ok(ex)
431 })
432 })
433 };
434
435 let pipeline = compose_pipeline(
436 vec![CompiledStep::Process {
437 kind_hint: SpanKindHint::Internal,
438 processor,
439 body_contract: None,
440 lifecycle: None,
441 label: None,
442 to_uri: None,
443 }],
444 PipelineRuntimeCtx::compile_time(),
445 );
446
447 let concurrency: u32 = 10;
448 let mut handles = Vec::with_capacity(concurrency as usize);
449 for i in 0..concurrency {
450 let p = pipeline.clone();
451 handles.push(tokio::spawn(async move {
452 let ex = Exchange::new(Message::new(format!("msg-{i}")));
453 p.oneshot(ex).await.unwrap()
454 }));
455 }
456
457 for h in handles {
458 let _ = h.await.unwrap();
459 }
460
461 assert_eq!(counter.load(Ordering::Relaxed), concurrency);
462 }
463
464 #[tokio::test]
467 #[allow(deprecated)]
468 async fn tst006_error_handler_invoked_on_failure() {
469 use camel_api::error_handler::ExceptionPolicy;
470 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message};
471 use camel_processor::ErrorHandlerService;
472 use std::sync::Arc;
473 use tower::ServiceExt;
474
475 let error_received = Arc::new(std::sync::Mutex::new(false));
476 let error_received_clone = Arc::clone(&error_received);
477
478 let handler: BoxProcessor = BoxProcessor::from_fn(move |ex: Exchange| {
480 let r = Arc::clone(&error_received_clone);
481 Box::pin(async move {
482 *r.lock().unwrap() = true;
483 Ok(ex)
484 })
485 });
486
487 let failing: BoxProcessor = BoxProcessor::from_fn(|_| {
489 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
490 });
491
492 let policy = ExceptionPolicy::new(|_| true);
493 let svc = ErrorHandlerService::new(failing, Some(handler), vec![(policy, None)]);
494 let ex = Exchange::new(Message::new("test"));
495 let result = svc.oneshot(ex).await;
496
497 assert!(result.is_ok(), "error handler should absorb the error");
498 assert!(
499 result.unwrap().has_error(),
500 "exchange should have error set"
501 );
502 assert!(
503 *error_received.lock().unwrap(),
504 "error handler processor should have been invoked"
505 );
506 }
507
508 #[tokio::test]
511 #[allow(deprecated)]
512 async fn tst007_dead_letter_channel_receives_failed_exchange() {
513 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message};
514 use camel_processor::ErrorHandlerService;
515 use std::sync::Arc;
516 use tower::ServiceExt;
517
518 let dlc_received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
519 let dlc_received_clone = Arc::clone(&dlc_received);
520
521 let dlc: BoxProcessor = BoxProcessor::from_fn(move |ex: Exchange| {
523 let r = Arc::clone(&dlc_received_clone);
524 Box::pin(async move {
525 r.lock().unwrap().push(ex.clone());
526 Ok(ex)
527 })
528 });
529
530 let failing: BoxProcessor = BoxProcessor::from_fn(|_| {
531 Box::pin(async { Err(CamelError::ProcessorError("fail".into())) })
532 });
533
534 let svc = ErrorHandlerService::new(failing, Some(dlc), vec![]);
535 let ex = Exchange::new(Message::new("dlc-test"));
536 let result = svc.oneshot(ex).await;
537
538 assert!(result.is_ok());
539 let exchanges = dlc_received.lock().unwrap();
540 assert_eq!(
541 exchanges.len(),
542 1,
543 "DLC should have received exactly one exchange"
544 );
545 assert!(exchanges[0].has_error());
546 }
547
548 #[tokio::test]
551 async fn tst008_header_propagation_across_processors() {
552 use camel_api::{Body, BoxProcessor, BoxProcessorExt, Exchange, Message, Value};
553 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
554 use tower::ServiceExt;
555
556 let step1: BoxProcessor = BoxProcessor::from_fn(|mut ex: Exchange| {
557 Box::pin(async move {
558 ex.input
559 .set_header("trace-id", Value::String("abc-123".into()));
560 Ok(ex)
561 })
562 });
563
564 let step2: BoxProcessor = BoxProcessor::from_fn(|mut ex: Exchange| {
565 Box::pin(async move {
566 ex.input.body = Body::Text("processed".to_string());
568 Ok(ex)
569 })
570 });
571
572 let pipeline = compose_pipeline(
573 vec![
574 CompiledStep::Process {
575 kind_hint: SpanKindHint::Internal,
576 processor: step1,
577 body_contract: None,
578 lifecycle: None,
579 label: None,
580 to_uri: None,
581 },
582 CompiledStep::Process {
583 kind_hint: SpanKindHint::Internal,
584 processor: step2,
585 body_contract: None,
586 lifecycle: None,
587 label: None,
588 to_uri: None,
589 },
590 ],
591 PipelineRuntimeCtx::compile_time(),
592 );
593 let ex = Exchange::new(Message::new("input"));
594 let result = pipeline.oneshot(ex).await.unwrap();
595
596 assert_eq!(
597 result.input.header("trace-id"),
598 Some(&Value::String("abc-123".into())),
599 "header should survive across processors"
600 );
601 assert_eq!(result.input.body.as_text(), Some("processed"));
602 }
603
604 #[tokio::test]
607 async fn tst009_exchange_body_type_conversion() {
608 use camel_api::body::Body;
609 use camel_api::body_converter::{BodyType, convert};
610
611 let text_body = Body::Text("hello".to_string());
613 let bytes_body = convert(text_body, BodyType::Bytes).unwrap();
614 assert!(matches!(bytes_body, Body::Bytes(_)));
615 if let Body::Bytes(ref b) = bytes_body {
616 assert_eq!(b.as_ref(), b"hello");
617 }
618
619 let text_body_back = convert(bytes_body, BodyType::Text).unwrap();
621 assert!(matches!(text_body_back, Body::Text(_)));
622 assert_eq!(text_body_back.as_text(), Some("hello"));
623 }
624
625 #[tokio::test]
628 async fn tst010_multicast_delivers_to_multiple_endpoints() {
629 use camel_api::multicast::{MulticastConfig, MulticastStrategy};
630 use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, Message};
631 use camel_processor::MulticastService;
632 use std::sync::Arc;
633 use tower::ServiceExt;
634
635 let received_a = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
636 let received_b = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
637
638 let endpoint_a: BoxProcessor = {
639 let r = Arc::clone(&received_a);
640 BoxProcessor::from_fn(move |ex: Exchange| {
641 let r = Arc::clone(&r);
642 Box::pin(async move {
643 r.lock().unwrap().push(ex.clone());
644 Ok(ex)
645 })
646 })
647 };
648
649 let endpoint_b: BoxProcessor = {
650 let r = Arc::clone(&received_b);
651 BoxProcessor::from_fn(move |ex: Exchange| {
652 let r = Arc::clone(&r);
653 Box::pin(async move {
654 r.lock().unwrap().push(ex.clone());
655 Ok(ex)
656 })
657 })
658 };
659
660 let config = MulticastConfig::new().aggregation(MulticastStrategy::LastWins);
661
662 let svc = MulticastService::new(vec![endpoint_a, endpoint_b], config)
663 .expect("multicast service creation should succeed");
664 let ex = Exchange::new(Message::new("multicast-test"));
665 let _result = svc.oneshot(ex).await.unwrap();
666
667 assert_eq!(
668 received_a.lock().unwrap().len(),
669 1,
670 "endpoint A should receive exactly one exchange"
671 );
672 assert_eq!(
673 received_b.lock().unwrap().len(),
674 1,
675 "endpoint B should receive exactly one exchange"
676 );
677 }
678}