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 }],
443 PipelineRuntimeCtx::compile_time(),
444 );
445
446 let concurrency: u32 = 10;
447 let mut handles = Vec::with_capacity(concurrency as usize);
448 for i in 0..concurrency {
449 let p = pipeline.clone();
450 handles.push(tokio::spawn(async move {
451 let ex = Exchange::new(Message::new(format!("msg-{i}")));
452 p.oneshot(ex).await.unwrap()
453 }));
454 }
455
456 for h in handles {
457 let _ = h.await.unwrap();
458 }
459
460 assert_eq!(counter.load(Ordering::Relaxed), concurrency);
461 }
462
463 #[tokio::test]
466 #[allow(deprecated)]
467 async fn tst006_error_handler_invoked_on_failure() {
468 use camel_api::error_handler::ExceptionPolicy;
469 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message};
470 use camel_processor::ErrorHandlerService;
471 use std::sync::Arc;
472 use tower::ServiceExt;
473
474 let error_received = Arc::new(std::sync::Mutex::new(false));
475 let error_received_clone = Arc::clone(&error_received);
476
477 let handler: BoxProcessor = BoxProcessor::from_fn(move |ex: Exchange| {
479 let r = Arc::clone(&error_received_clone);
480 Box::pin(async move {
481 *r.lock().unwrap() = true;
482 Ok(ex)
483 })
484 });
485
486 let failing: BoxProcessor = BoxProcessor::from_fn(|_| {
488 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
489 });
490
491 let policy = ExceptionPolicy::new(|_| true);
492 let svc = ErrorHandlerService::new(failing, Some(handler), vec![(policy, None)]);
493 let ex = Exchange::new(Message::new("test"));
494 let result = svc.oneshot(ex).await;
495
496 assert!(result.is_ok(), "error handler should absorb the error");
497 assert!(
498 result.unwrap().has_error(),
499 "exchange should have error set"
500 );
501 assert!(
502 *error_received.lock().unwrap(),
503 "error handler processor should have been invoked"
504 );
505 }
506
507 #[tokio::test]
510 #[allow(deprecated)]
511 async fn tst007_dead_letter_channel_receives_failed_exchange() {
512 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message};
513 use camel_processor::ErrorHandlerService;
514 use std::sync::Arc;
515 use tower::ServiceExt;
516
517 let dlc_received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
518 let dlc_received_clone = Arc::clone(&dlc_received);
519
520 let dlc: BoxProcessor = BoxProcessor::from_fn(move |ex: Exchange| {
522 let r = Arc::clone(&dlc_received_clone);
523 Box::pin(async move {
524 r.lock().unwrap().push(ex.clone());
525 Ok(ex)
526 })
527 });
528
529 let failing: BoxProcessor = BoxProcessor::from_fn(|_| {
530 Box::pin(async { Err(CamelError::ProcessorError("fail".into())) })
531 });
532
533 let svc = ErrorHandlerService::new(failing, Some(dlc), vec![]);
534 let ex = Exchange::new(Message::new("dlc-test"));
535 let result = svc.oneshot(ex).await;
536
537 assert!(result.is_ok());
538 let exchanges = dlc_received.lock().unwrap();
539 assert_eq!(
540 exchanges.len(),
541 1,
542 "DLC should have received exactly one exchange"
543 );
544 assert!(exchanges[0].has_error());
545 }
546
547 #[tokio::test]
550 async fn tst008_header_propagation_across_processors() {
551 use camel_api::{Body, BoxProcessor, BoxProcessorExt, Exchange, Message, Value};
552 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
553 use tower::ServiceExt;
554
555 let step1: BoxProcessor = BoxProcessor::from_fn(|mut ex: Exchange| {
556 Box::pin(async move {
557 ex.input
558 .set_header("trace-id", Value::String("abc-123".into()));
559 Ok(ex)
560 })
561 });
562
563 let step2: BoxProcessor = BoxProcessor::from_fn(|mut ex: Exchange| {
564 Box::pin(async move {
565 ex.input.body = Body::Text("processed".to_string());
567 Ok(ex)
568 })
569 });
570
571 let pipeline = compose_pipeline(
572 vec![
573 CompiledStep::Process {
574 kind_hint: SpanKindHint::Internal,
575 processor: step1,
576 body_contract: None,
577 lifecycle: None,
578 label: None,
579 },
580 CompiledStep::Process {
581 kind_hint: SpanKindHint::Internal,
582 processor: step2,
583 body_contract: None,
584 lifecycle: None,
585 label: None,
586 },
587 ],
588 PipelineRuntimeCtx::compile_time(),
589 );
590 let ex = Exchange::new(Message::new("input"));
591 let result = pipeline.oneshot(ex).await.unwrap();
592
593 assert_eq!(
594 result.input.header("trace-id"),
595 Some(&Value::String("abc-123".into())),
596 "header should survive across processors"
597 );
598 assert_eq!(result.input.body.as_text(), Some("processed"));
599 }
600
601 #[tokio::test]
604 async fn tst009_exchange_body_type_conversion() {
605 use camel_api::body::Body;
606 use camel_api::body_converter::{BodyType, convert};
607
608 let text_body = Body::Text("hello".to_string());
610 let bytes_body = convert(text_body, BodyType::Bytes).unwrap();
611 assert!(matches!(bytes_body, Body::Bytes(_)));
612 if let Body::Bytes(ref b) = bytes_body {
613 assert_eq!(b.as_ref(), b"hello");
614 }
615
616 let text_body_back = convert(bytes_body, BodyType::Text).unwrap();
618 assert!(matches!(text_body_back, Body::Text(_)));
619 assert_eq!(text_body_back.as_text(), Some("hello"));
620 }
621
622 #[tokio::test]
625 async fn tst010_multicast_delivers_to_multiple_endpoints() {
626 use camel_api::multicast::{MulticastConfig, MulticastStrategy};
627 use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, Message};
628 use camel_processor::MulticastService;
629 use std::sync::Arc;
630 use tower::ServiceExt;
631
632 let received_a = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
633 let received_b = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
634
635 let endpoint_a: BoxProcessor = {
636 let r = Arc::clone(&received_a);
637 BoxProcessor::from_fn(move |ex: Exchange| {
638 let r = Arc::clone(&r);
639 Box::pin(async move {
640 r.lock().unwrap().push(ex.clone());
641 Ok(ex)
642 })
643 })
644 };
645
646 let endpoint_b: BoxProcessor = {
647 let r = Arc::clone(&received_b);
648 BoxProcessor::from_fn(move |ex: Exchange| {
649 let r = Arc::clone(&r);
650 Box::pin(async move {
651 r.lock().unwrap().push(ex.clone());
652 Ok(ex)
653 })
654 })
655 };
656
657 let config = MulticastConfig::new().aggregation(MulticastStrategy::LastWins);
658
659 let svc = MulticastService::new(vec![endpoint_a, endpoint_b], config)
660 .expect("multicast service creation should succeed");
661 let ex = Exchange::new(Message::new("multicast-test"));
662 let _result = svc.oneshot(ex).await.unwrap();
663
664 assert_eq!(
665 received_a.lock().unwrap().len(),
666 1,
667 "endpoint A should receive exactly one exchange"
668 );
669 assert_eq!(
670 received_b.lock().unwrap().len(),
671 1,
672 "endpoint B should receive exactly one exchange"
673 );
674 }
675}