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_timer(mut self) -> Self {
89 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
90 ctx.register_component(TimerComponent::new());
91 }));
92 self
93 }
94
95 pub fn with_log(mut self) -> Self {
97 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
98 ctx.register_component(LogComponent::new());
99 }));
100 self
101 }
102
103 pub fn with_direct(mut self) -> Self {
105 self.registrations.push(Box::new(|ctx: &mut CamelContext| {
106 ctx.register_component(DirectComponent::new());
107 }));
108 self
109 }
110
111 pub fn with_component<C>(mut self, component: C) -> Self
113 where
114 C: camel_component_api::Component + 'static,
115 {
116 self.registrations
117 .push(Box::new(move |ctx: &mut CamelContext| {
118 ctx.register_component(component);
119 }));
120 self
121 }
122 }
123 };
124}
125
126impl_builder_methods!(NoTimeControl);
127impl_builder_methods!(WithTimeControl);
128
129async fn build_context(registrations: Vec<Registration>, mock: MockComponent) -> CamelTestContext {
134 let mut ctx = CamelContext::builder().build().await.unwrap();
135
136 ctx.register_component(mock.clone());
138
139 for register in registrations {
141 register(&mut ctx);
142 }
143
144 let ctx = Arc::new(Mutex::new(ctx));
145 let stopped = Arc::new(AtomicBool::new(false));
146
147 CamelTestContext {
148 ctx: ctx.clone(),
149 mock,
150 stopped: stopped.clone(),
151 _guard: TestGuard { ctx, stopped },
152 }
153}
154
155pub(crate) struct TestGuard {
160 ctx: Arc<Mutex<CamelContext>>,
161 stopped: Arc<AtomicBool>,
162}
163
164impl Drop for TestGuard {
165 fn drop(&mut self) {
166 if self.stopped.swap(true, Ordering::SeqCst) {
167 return;
169 }
170 let ctx = self.ctx.clone();
171 if let Ok(handle) = tokio::runtime::Handle::try_current() {
172 match handle.runtime_flavor() {
173 tokio::runtime::RuntimeFlavor::MultiThread => {
174 tokio::task::block_in_place(|| {
176 handle.block_on(async move {
177 let mut ctx = ctx.lock().await;
178 let _ = ctx.stop().await;
179 });
180 });
181 }
182 tokio::runtime::RuntimeFlavor::CurrentThread => {
183 handle.spawn(async move {
186 let mut ctx = ctx.lock().await;
187 let _ = ctx.stop().await;
188 });
189 }
190 _ => {
191 handle.spawn(async move {
192 let mut ctx = ctx.lock().await;
193 let _ = ctx.stop().await;
194 });
195 }
196 }
197 }
198 }
199}
200
201pub struct CamelTestContext {
227 ctx: Arc<Mutex<CamelContext>>,
228 mock: MockComponent,
229 stopped: Arc<AtomicBool>,
230 _guard: TestGuard,
231}
232
233impl CamelTestContext {
234 pub fn builder() -> CamelTestContextBuilder<NoTimeControl> {
236 CamelTestContextBuilder::new()
237 }
238
239 pub async fn add_route(&self, route: RouteDefinition) -> Result<(), CamelError> {
241 let ctx = self.ctx.lock().await;
242 ctx.add_route_definition(route).await
243 }
244
245 pub async fn start(&self) {
247 let mut ctx = self.ctx.lock().await;
248 ctx.start().await.expect("CamelTestContext: start failed");
249 }
250
251 pub async fn stop(&self) {
254 if self.stopped.swap(true, Ordering::SeqCst) {
255 return; }
257 let mut ctx = self.ctx.lock().await;
258 ctx.stop().await.expect("CamelTestContext: stop failed");
259 }
260
261 pub async fn shutdown(self) {
263 self.stop().await;
264 }
265
266 pub fn mock(&self) -> &MockComponent {
268 &self.mock
269 }
270
271 pub fn ctx(&self) -> &Arc<Mutex<CamelContext>> {
273 &self.ctx
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280 use camel_builder::{RouteBuilder, StepAccumulator};
281 use std::time::Duration;
282
283 #[tokio::test]
284 async fn builder_without_time_control_builds_context() {
285 let harness = CamelTestContext::builder()
286 .with_mock()
287 .with_timer()
288 .with_log()
289 .build()
290 .await;
291
292 assert!(harness.mock().get_endpoint("result").is_none());
293 let guard = harness.ctx().lock().await;
294 let _ = &*guard;
295 }
296
297 #[tokio::test]
298 async fn builder_with_time_control_builds_and_advances_clock() {
299 let (_harness, time) = CamelTestContext::builder()
300 .with_mock()
301 .with_timer()
302 .with_time_control()
303 .build()
304 .await;
305
306 time.advance(Duration::from_millis(1)).await;
307 time.resume();
308 }
309
310 #[tokio::test]
311 async fn stop_is_idempotent_and_shutdown_is_safe() {
312 let harness = CamelTestContext::builder().with_mock().build().await;
313 harness.stop().await;
314 harness.stop().await;
315 harness.shutdown().await;
316 }
317
318 #[tokio::test]
319 async fn add_route_returns_error_for_invalid_step_uri() {
320 let harness = CamelTestContext::builder().with_mock().build().await;
321
322 let route = RouteBuilder::from("direct:start")
323 .route_id("bad-route")
324 .to("not-a-uri")
325 .build()
326 .unwrap();
327
328 let err = harness.add_route(route).await.expect_err("must fail");
329 assert!(err.to_string().contains("Invalid") || err.to_string().contains("invalid"));
330 }
331
332 #[tokio::test]
333 async fn with_component_registers_custom_component() {
334 let harness = CamelTestContext::builder()
335 .with_component(camel_component_direct::DirectComponent::new())
336 .with_mock()
337 .build()
338 .await;
339
340 let route = RouteBuilder::from("direct:start")
341 .route_id("direct-route")
342 .to("mock:out")
343 .build()
344 .unwrap();
345
346 harness.add_route(route).await.unwrap();
347 harness.start().await;
348 harness.stop().await;
349
350 let _guard = harness.ctx().lock().await;
352 }
353}