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