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