clnrm_core/
macros.rs

1//! Jane-friendly macros for cleanroom testing
2//!
3//! This module provides a high-level, declarative API that abstracts away
4//! the complexity of container management and provides the developer experience
5//! that users actually want.
6
7use crate::cleanroom::{CleanroomEnvironment, HealthStatus, ServiceHandle, ServicePlugin};
8use crate::error::{CleanroomError, Result};
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// Jane-friendly test macro that handles all the boilerplate
14///
15/// This macro provides zero-boilerplate testing with automatic:
16/// - Container lifecycle management
17/// - Service setup and teardown
18/// - Error handling and reporting
19/// - OTel tracing and metrics
20///
21/// # Example
22///
23/// ```rust
24/// use clnrm::{cleanroom_test, with_database, with_cache};
25///
26/// #[cleanroom_test]
27/// async fn test_user_registration() {
28///     with_database("postgres:15");
29///     with_cache("redis:7");
30///     
31///     let user = register_user("jane@example.com")?;
32///     assert!(user.id > 0);
33/// }
34/// ```
35#[macro_export]
36macro_rules! cleanroom_test {
37    ($(#[$meta:meta])* $vis:vis async fn $name:ident() $body:block) => {
38        $(#[$meta])*
39        #[tokio::test(flavor = "multi_thread")]
40        $vis async fn $name() -> Result<(), $crate::error::CleanroomError> {
41            // Initialize cleanroom environment
42            let env = $crate::cleanroom::CleanroomEnvironment::new().await
43                .map_err(|e| $crate::error::CleanroomError::internal_error("Failed to create cleanroom environment")
44                    .with_context("Cleanroom environment initialization failed")
45                    .with_source(e.to_string())
46                )?;
47
48            // Set up test context
49            let mut test_context = $crate::macros::TestContext::new(env);
50
51            // Run the test with proper error handling
52            let result = async {
53                $body
54            }.await;
55
56            // Handle test result with clear error messages
57            match result {
58                Ok(_) => {
59                    tracing::info!(test_name = stringify!($name), "Test passed");
60                    Ok(())
61                }
62                Err(e) => {
63                    tracing::error!(
64                        test_name = stringify!($name),
65                        error = %e,
66                        "Test failed"
67                    );
68                    tracing::debug!("Debug info: Check if required Docker images are available");
69                    tracing::debug!("Debug info: Verify services are running correctly");
70                    tracing::debug!("Debug info: Check container logs for more details");
71                    Err(e)
72                }
73            }
74        }
75    };
76}
77
78/// Declarative service setup - Jane's one-liner service management
79///
80/// This provides the simple, declarative API that Jane wants:
81/// - `with_database("postgres:15")` - starts postgres container
82/// - `with_cache("redis:7")` - starts redis container
83/// - `with_message_queue("rabbitmq")` - starts rabbitmq container
84///
85/// Services are automatically managed with proper lifecycle and health checks.
86pub struct ServiceSetup {
87    env: Arc<CleanroomEnvironment>,
88    services: Arc<RwLock<HashMap<String, ServiceHandle>>>,
89}
90
91impl ServiceSetup {
92    /// Create a new service setup context
93    pub fn new(env: Arc<CleanroomEnvironment>) -> Self {
94        Self {
95            env,
96            services: Arc::new(RwLock::new(HashMap::new())),
97        }
98    }
99
100    /// Set up a database service
101    pub async fn with_database(&self, image: &str) -> Result<()> {
102        self.with_service(
103            "database",
104            image,
105            Box::new(DatabaseServicePlugin::new(image)),
106        )
107        .await
108    }
109
110    /// Set up a cache service
111    pub async fn with_cache(&self, image: &str) -> Result<()> {
112        self.with_service("cache", image, Box::new(CacheServicePlugin::new(image)))
113            .await
114    }
115
116    /// Set up a message queue service
117    pub async fn with_message_queue(&self, image: &str) -> Result<()> {
118        self.with_service(
119            "message_queue",
120            image,
121            Box::new(MessageQueueServicePlugin::new(image)),
122        )
123        .await
124    }
125
126    /// Set up a web server service
127    pub async fn with_web_server(&self, image: &str) -> Result<()> {
128        self.with_service(
129            "web_server",
130            image,
131            Box::new(WebServerServicePlugin::new(image)),
132        )
133        .await
134    }
135
136    /// Generic service setup
137    async fn with_service(
138        &self,
139        service_type: &str,
140        image: &str,
141        plugin: Box<dyn ServicePlugin>,
142    ) -> Result<()> {
143        tracing::info!(
144            service_type = %service_type,
145            image = %image,
146            "Starting service"
147        );
148
149        // Register the service plugin
150        self.env.register_service(plugin).await?;
151
152        // Start the service
153        let handle = self.env.start_service(service_type).await?;
154
155        // Store the handle for cleanup
156        let mut services = self.services.write().await;
157        services.insert(service_type.to_string(), handle);
158
159        tracing::info!(
160            service_type = %service_type,
161            "Service started successfully"
162        );
163        Ok(())
164    }
165
166    /// Get service connection info (for Jane's test code)
167    pub async fn get_database_url(&self) -> Result<String> {
168        let services = self.services.read().await;
169        if let Some(handle) = services.get("database") {
170            let default_port = "5432".to_string();
171            let port = handle.metadata.get("port").unwrap_or(&default_port);
172            Ok(format!(
173                "postgresql://postgres:password@localhost:{}/testdb",
174                port
175            ))
176        } else {
177            Err(CleanroomError::internal_error(
178                "Database service not started. Call with_database() first.",
179            ))
180        }
181    }
182
183    /// Get cache connection info
184    pub async fn get_cache_url(&self) -> Result<String> {
185        let services = self.services.read().await;
186        if let Some(handle) = services.get("cache") {
187            let default_port = "6379".to_string();
188            let port = handle.metadata.get("port").unwrap_or(&default_port);
189            Ok(format!("redis://localhost:{}", port))
190        } else {
191            Err(CleanroomError::internal_error(
192                "Cache service not started. Call with_cache() first.",
193            ))
194        }
195    }
196}
197
198/// Test context that provides Jane-friendly APIs
199pub struct TestContext {
200    env: Arc<CleanroomEnvironment>,
201    services: ServiceSetup,
202}
203
204impl TestContext {
205    /// Create a new test context
206    pub fn new(env: CleanroomEnvironment) -> Self {
207        let env = Arc::new(env);
208        let services = ServiceSetup::new(env.clone());
209
210        Self { env, services }
211    }
212
213    /// Get service setup for declarative configuration
214    pub fn services(&self) -> &ServiceSetup {
215        &self.services
216    }
217
218    /// Get the underlying cleanroom environment
219    pub fn env(&self) -> &Arc<CleanroomEnvironment> {
220        &self.env
221    }
222}
223
224/// Database service plugin implementation
225#[derive(Debug)]
226pub struct DatabaseServicePlugin {
227    name: String,
228    image: String,
229}
230
231impl DatabaseServicePlugin {
232    pub fn new(image: &str) -> Self {
233        Self {
234            name: "database".to_string(),
235            image: image.to_string(),
236        }
237    }
238}
239
240impl ServicePlugin for DatabaseServicePlugin {
241    fn name(&self) -> &str {
242        &self.name
243    }
244
245    fn start(&self) -> Result<ServiceHandle> {
246        // In a real implementation, this would start the database container
247        // and return connection details
248        Ok(ServiceHandle {
249            id: format!("db_{}", uuid::Uuid::new_v4()),
250            service_name: self.name.clone(),
251            metadata: HashMap::from([
252                ("type".to_string(), "database".to_string()),
253                ("image".to_string(), self.image.clone()),
254                ("port".to_string(), "5432".to_string()),
255                ("status".to_string(), "running".to_string()),
256            ]),
257        })
258    }
259
260    fn stop(&self, _handle: ServiceHandle) -> Result<()> {
261        // In a real implementation, this would stop the database container
262        Ok(())
263    }
264
265    fn health_check(&self, _handle: &ServiceHandle) -> HealthStatus {
266        // In a real implementation, this would check if the database is responding
267        HealthStatus::Healthy
268    }
269}
270
271/// Cache service plugin implementation
272#[derive(Debug)]
273pub struct CacheServicePlugin {
274    name: String,
275    image: String,
276}
277
278impl CacheServicePlugin {
279    pub fn new(image: &str) -> Self {
280        Self {
281            name: "cache".to_string(),
282            image: image.to_string(),
283        }
284    }
285}
286
287impl ServicePlugin for CacheServicePlugin {
288    fn name(&self) -> &str {
289        &self.name
290    }
291
292    fn start(&self) -> Result<ServiceHandle> {
293        Ok(ServiceHandle {
294            id: format!("cache_{}", uuid::Uuid::new_v4()),
295            service_name: self.name.clone(),
296            metadata: HashMap::from([
297                ("type".to_string(), "cache".to_string()),
298                ("image".to_string(), self.image.clone()),
299                ("port".to_string(), "6379".to_string()),
300                ("status".to_string(), "running".to_string()),
301            ]),
302        })
303    }
304
305    fn stop(&self, _handle: ServiceHandle) -> Result<()> {
306        Ok(())
307    }
308
309    fn health_check(&self, _handle: &ServiceHandle) -> HealthStatus {
310        HealthStatus::Healthy
311    }
312}
313
314/// Message queue service plugin implementation
315#[derive(Debug)]
316pub struct MessageQueueServicePlugin {
317    name: String,
318    image: String,
319}
320
321impl MessageQueueServicePlugin {
322    pub fn new(image: &str) -> Self {
323        Self {
324            name: "message_queue".to_string(),
325            image: image.to_string(),
326        }
327    }
328}
329
330impl ServicePlugin for MessageQueueServicePlugin {
331    fn name(&self) -> &str {
332        &self.name
333    }
334
335    fn start(&self) -> Result<ServiceHandle> {
336        Ok(ServiceHandle {
337            id: format!("mq_{}", uuid::Uuid::new_v4()),
338            service_name: self.name.clone(),
339            metadata: HashMap::from([
340                ("type".to_string(), "message_queue".to_string()),
341                ("image".to_string(), self.image.clone()),
342                ("port".to_string(), "5672".to_string()),
343                ("status".to_string(), "running".to_string()),
344            ]),
345        })
346    }
347
348    fn stop(&self, _handle: ServiceHandle) -> Result<()> {
349        Ok(())
350    }
351
352    fn health_check(&self, _handle: &ServiceHandle) -> HealthStatus {
353        HealthStatus::Healthy
354    }
355}
356
357/// Web server service plugin implementation
358#[derive(Debug)]
359pub struct WebServerServicePlugin {
360    name: String,
361    image: String,
362}
363
364impl WebServerServicePlugin {
365    pub fn new(image: &str) -> Self {
366        Self {
367            name: "web_server".to_string(),
368            image: image.to_string(),
369        }
370    }
371}
372
373impl ServicePlugin for WebServerServicePlugin {
374    fn name(&self) -> &str {
375        &self.name
376    }
377
378    fn start(&self) -> Result<ServiceHandle> {
379        Ok(ServiceHandle {
380            id: format!("web_{}", uuid::Uuid::new_v4()),
381            service_name: self.name.clone(),
382            metadata: HashMap::from([
383                ("type".to_string(), "web_server".to_string()),
384                ("image".to_string(), self.image.clone()),
385                ("port".to_string(), "8080".to_string()),
386                ("status".to_string(), "running".to_string()),
387            ]),
388        })
389    }
390
391    fn stop(&self, _handle: ServiceHandle) -> Result<()> {
392        Ok(())
393    }
394
395    fn health_check(&self, _handle: &ServiceHandle) -> HealthStatus {
396        HealthStatus::Healthy
397    }
398}
399
400/// Jane-friendly service setup functions
401/// These provide the simple, declarative API that Jane wants
402///
403/// Set up a database service with the specified image
404pub async fn with_database(image: &str) -> Result<()> {
405    tracing::info!("Setting up database service with image: {}", image);
406
407    // In a real implementation, this would:
408    // 1. Create a database container using the specified image
409    // 2. Wait for the database to be ready
410    // 3. Set up connection configuration
411    // 4. Return connection details
412
413    // For now, just log the setup
414    tracing::info!(
415        "Database service '{}' setup completed (placeholder implementation)",
416        image
417    );
418
419    // In a real implementation, this would return connection details
420    // and the service would be managed by the test framework
421    Ok(())
422}
423
424/// Set up a cache service with the specified image
425pub async fn with_cache(image: &str) -> Result<()> {
426    tracing::info!("Setting up cache service with image: {}", image);
427
428    // In a real implementation, this would:
429    // 1. Create a cache container (Redis, Memcached, etc.)
430    // 2. Configure cache settings
431    // 3. Wait for cache to be ready
432    // 4. Return connection details
433
434    tracing::info!(
435        "Cache service '{}' setup completed (placeholder implementation)",
436        image
437    );
438    Ok(())
439}
440
441/// Set up a message queue service with the specified image
442pub async fn with_message_queue(image: &str) -> Result<()> {
443    tracing::info!("Setting up message queue service with image: {}", image);
444
445    // In a real implementation, this would:
446    // 1. Create a message queue container (RabbitMQ, Kafka, etc.)
447    // 2. Configure queue settings
448    // 3. Wait for queue to be ready
449    // 4. Return connection details
450
451    tracing::info!(
452        "Message queue service '{}' setup completed (placeholder implementation)",
453        image
454    );
455    Ok(())
456}
457
458/// Set up a web server service with the specified image
459pub async fn with_web_server(image: &str) -> Result<()> {
460    tracing::info!("Setting up web server service with image: {}", image);
461
462    // In a real implementation, this would:
463    // 1. Create a web server container (nginx, Apache, etc.)
464    // 2. Configure server settings
465    // 3. Wait for server to be ready
466    // 4. Return connection details
467
468    tracing::info!(
469        "Web server service '{}' setup completed (placeholder implementation)",
470        image
471    );
472    Ok(())
473}