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_builder::{RouteBuilder, StepAccumulator};
306 use std::time::Duration;
307
308 #[tokio::test]
309 async fn builder_without_time_control_builds_context() {
310 let harness = CamelTestContext::builder()
311 .with_mock()
312 .with_timer()
313 .with_log()
314 .build()
315 .await;
316
317 assert!(harness.mock().get_endpoint("result").is_none());
318 let guard = harness.ctx().lock().await;
319 let _ = &*guard;
320 }
321
322 #[tokio::test]
323 async fn builder_with_time_control_builds_and_advances_clock() {
324 let (_harness, time) = CamelTestContext::builder()
325 .with_mock()
326 .with_timer()
327 .with_time_control()
328 .build()
329 .await;
330
331 time.advance(Duration::from_millis(1)).await;
332 time.resume();
333 }
334
335 #[tokio::test]
336 async fn stop_is_idempotent_and_shutdown_is_safe() {
337 let harness = CamelTestContext::builder().with_mock().build().await;
338 harness.stop().await;
339 harness.stop().await;
340 harness.shutdown().await;
341 }
342
343 #[tokio::test]
344 async fn add_route_returns_error_for_invalid_step_uri() {
345 let harness = CamelTestContext::builder().with_mock().build().await;
346
347 let route = RouteBuilder::from("direct:start")
348 .route_id("bad-route")
349 .to("not-a-uri")
350 .build()
351 .unwrap();
352
353 let err = harness.add_route(route).await.expect_err("must fail");
354 assert!(err.to_string().contains("Invalid") || err.to_string().contains("invalid"));
355 }
356
357 #[tokio::test]
358 async fn with_component_registers_custom_component() {
359 let harness = CamelTestContext::builder()
360 .with_component(camel_component_direct::DirectComponent::new())
361 .with_mock()
362 .build()
363 .await;
364
365 let route = RouteBuilder::from("direct:start")
366 .route_id("direct-route")
367 .to("mock:out")
368 .build()
369 .unwrap();
370
371 harness.add_route(route).await.unwrap();
372 harness.start().await;
373 harness.stop().await;
374
375 let _guard = harness.ctx().lock().await;
377 }
378
379 #[tokio::test]
382 async fn tst004_route_lifecycle_start_stop_restart() {
383 let harness = CamelTestContext::builder()
384 .with_direct()
385 .with_mock()
386 .build()
387 .await;
388
389 let route = RouteBuilder::from("direct:lifecycle")
390 .route_id("lifecycle-route")
391 .to("mock:lifecycle-out")
392 .build()
393 .unwrap();
394
395 harness.add_route(route).await.unwrap();
396
397 harness.start().await;
399
400 harness.stop().await;
402
403 {
405 let mut ctx = harness.ctx().lock().await;
406 ctx.start().await.expect("restart should succeed");
407 ctx.stop().await.expect("stop after restart should succeed");
408 }
409 }
410
411 #[tokio::test]
414 async fn tst005_concurrent_exchange_processing() {
415 use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, Message};
416 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
417 use std::sync::Arc;
418 use std::sync::atomic::{AtomicU32, Ordering};
419 use tower::ServiceExt;
420
421 let counter = Arc::new(AtomicU32::new(0));
422 let processor: BoxProcessor = {
423 let c = Arc::clone(&counter);
424 BoxProcessor::from_fn(move |ex: Exchange| {
425 let c = Arc::clone(&c);
426 Box::pin(async move {
427 c.fetch_add(1, Ordering::Relaxed);
428 tokio::task::yield_now().await;
429 Ok(ex)
430 })
431 })
432 };
433
434 let pipeline = compose_pipeline(
435 vec![CompiledStep::Process {
436 processor,
437 body_contract: None,
438 lifecycle: None,
439 }],
440 PipelineRuntimeCtx::compile_time(),
441 );
442
443 let concurrency: u32 = 10;
444 let mut handles = Vec::with_capacity(concurrency as usize);
445 for i in 0..concurrency {
446 let p = pipeline.clone();
447 handles.push(tokio::spawn(async move {
448 let ex = Exchange::new(Message::new(format!("msg-{i}")));
449 p.oneshot(ex).await.unwrap()
450 }));
451 }
452
453 for h in handles {
454 let _ = h.await.unwrap();
455 }
456
457 assert_eq!(counter.load(Ordering::Relaxed), concurrency);
458 }
459
460 #[tokio::test]
463 #[allow(deprecated)]
464 async fn tst006_error_handler_invoked_on_failure() {
465 use camel_api::error_handler::ExceptionPolicy;
466 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message};
467 use camel_processor::ErrorHandlerService;
468 use std::sync::Arc;
469 use tower::ServiceExt;
470
471 let error_received = Arc::new(std::sync::Mutex::new(false));
472 let error_received_clone = Arc::clone(&error_received);
473
474 let handler: BoxProcessor = BoxProcessor::from_fn(move |ex: Exchange| {
476 let r = Arc::clone(&error_received_clone);
477 Box::pin(async move {
478 *r.lock().unwrap() = true;
479 Ok(ex)
480 })
481 });
482
483 let failing: BoxProcessor = BoxProcessor::from_fn(|_| {
485 Box::pin(async { Err(CamelError::ProcessorError("boom".into())) })
486 });
487
488 let policy = ExceptionPolicy::new(|_| true);
489 let svc = ErrorHandlerService::new(failing, Some(handler), vec![(policy, None)]);
490 let ex = Exchange::new(Message::new("test"));
491 let result = svc.oneshot(ex).await;
492
493 assert!(result.is_ok(), "error handler should absorb the error");
494 assert!(
495 result.unwrap().has_error(),
496 "exchange should have error set"
497 );
498 assert!(
499 *error_received.lock().unwrap(),
500 "error handler processor should have been invoked"
501 );
502 }
503
504 #[tokio::test]
507 #[allow(deprecated)]
508 async fn tst007_dead_letter_channel_receives_failed_exchange() {
509 use camel_api::{BoxProcessor, BoxProcessorExt, CamelError, Exchange, Message};
510 use camel_processor::ErrorHandlerService;
511 use std::sync::Arc;
512 use tower::ServiceExt;
513
514 let dlc_received = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
515 let dlc_received_clone = Arc::clone(&dlc_received);
516
517 let dlc: BoxProcessor = BoxProcessor::from_fn(move |ex: Exchange| {
519 let r = Arc::clone(&dlc_received_clone);
520 Box::pin(async move {
521 r.lock().unwrap().push(ex.clone());
522 Ok(ex)
523 })
524 });
525
526 let failing: BoxProcessor = BoxProcessor::from_fn(|_| {
527 Box::pin(async { Err(CamelError::ProcessorError("fail".into())) })
528 });
529
530 let svc = ErrorHandlerService::new(failing, Some(dlc), vec![]);
531 let ex = Exchange::new(Message::new("dlc-test"));
532 let result = svc.oneshot(ex).await;
533
534 assert!(result.is_ok());
535 let exchanges = dlc_received.lock().unwrap();
536 assert_eq!(
537 exchanges.len(),
538 1,
539 "DLC should have received exactly one exchange"
540 );
541 assert!(exchanges[0].has_error());
542 }
543
544 #[tokio::test]
547 async fn tst008_header_propagation_across_processors() {
548 use camel_api::{Body, BoxProcessor, BoxProcessorExt, Exchange, Message, Value};
549 use camel_core::route::{CompiledStep, PipelineRuntimeCtx, compose_pipeline};
550 use tower::ServiceExt;
551
552 let step1: BoxProcessor = BoxProcessor::from_fn(|mut ex: Exchange| {
553 Box::pin(async move {
554 ex.input
555 .set_header("trace-id", Value::String("abc-123".into()));
556 Ok(ex)
557 })
558 });
559
560 let step2: BoxProcessor = BoxProcessor::from_fn(|mut ex: Exchange| {
561 Box::pin(async move {
562 ex.input.body = Body::Text("processed".to_string());
564 Ok(ex)
565 })
566 });
567
568 let pipeline = compose_pipeline(
569 vec![
570 CompiledStep::Process {
571 processor: step1,
572 body_contract: None,
573 lifecycle: None,
574 },
575 CompiledStep::Process {
576 processor: step2,
577 body_contract: None,
578 lifecycle: None,
579 },
580 ],
581 PipelineRuntimeCtx::compile_time(),
582 );
583 let ex = Exchange::new(Message::new("input"));
584 let result = pipeline.oneshot(ex).await.unwrap();
585
586 assert_eq!(
587 result.input.header("trace-id"),
588 Some(&Value::String("abc-123".into())),
589 "header should survive across processors"
590 );
591 assert_eq!(result.input.body.as_text(), Some("processed"));
592 }
593
594 #[tokio::test]
597 async fn tst009_exchange_body_type_conversion() {
598 use camel_api::body::Body;
599 use camel_api::body_converter::{BodyType, convert};
600
601 let text_body = Body::Text("hello".to_string());
603 let bytes_body = convert(text_body, BodyType::Bytes).unwrap();
604 assert!(matches!(bytes_body, Body::Bytes(_)));
605 if let Body::Bytes(ref b) = bytes_body {
606 assert_eq!(b.as_ref(), b"hello");
607 }
608
609 let text_body_back = convert(bytes_body, BodyType::Text).unwrap();
611 assert!(matches!(text_body_back, Body::Text(_)));
612 assert_eq!(text_body_back.as_text(), Some("hello"));
613 }
614
615 #[tokio::test]
618 async fn tst010_multicast_delivers_to_multiple_endpoints() {
619 use camel_api::multicast::{MulticastConfig, MulticastStrategy};
620 use camel_api::{BoxProcessor, BoxProcessorExt, Exchange, Message};
621 use camel_processor::MulticastService;
622 use std::sync::Arc;
623 use tower::ServiceExt;
624
625 let received_a = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
626 let received_b = Arc::new(std::sync::Mutex::new(Vec::<Exchange>::new()));
627
628 let endpoint_a: BoxProcessor = {
629 let r = Arc::clone(&received_a);
630 BoxProcessor::from_fn(move |ex: Exchange| {
631 let r = Arc::clone(&r);
632 Box::pin(async move {
633 r.lock().unwrap().push(ex.clone());
634 Ok(ex)
635 })
636 })
637 };
638
639 let endpoint_b: BoxProcessor = {
640 let r = Arc::clone(&received_b);
641 BoxProcessor::from_fn(move |ex: Exchange| {
642 let r = Arc::clone(&r);
643 Box::pin(async move {
644 r.lock().unwrap().push(ex.clone());
645 Ok(ex)
646 })
647 })
648 };
649
650 let config = MulticastConfig::new().aggregation(MulticastStrategy::LastWins);
651
652 let svc = MulticastService::new(vec![endpoint_a, endpoint_b], config)
653 .expect("multicast service creation should succeed");
654 let ex = Exchange::new(Message::new("multicast-test"));
655 let _result = svc.oneshot(ex).await.unwrap();
656
657 assert_eq!(
658 received_a.lock().unwrap().len(),
659 1,
660 "endpoint A should receive exactly one exchange"
661 );
662 assert_eq!(
663 received_b.lock().unwrap().len(),
664 1,
665 "endpoint B should receive exactly one exchange"
666 );
667 }
668}