1use 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#[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 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 let mut test_context = $crate::macros::TestContext::new(env);
52
53 let result = async {
55 $body
56 }.await;
57
58 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
77pub struct ServiceSetup {
86 env: Arc<CleanroomEnvironment>,
87 services: Arc<RwLock<HashMap<String, ServiceHandle>>>,
88}
89
90impl ServiceSetup {
91 pub fn new(env: Arc<CleanroomEnvironment>) -> Self {
93 Self {
94 env,
95 services: Arc::new(RwLock::new(HashMap::new())),
96 }
97 }
98
99 pub async fn with_database(&self, image: &str) -> Result<()> {
101 self.with_service("database", image, Box::new(DatabaseServicePlugin::new(image))).await
102 }
103
104 pub async fn with_cache(&self, image: &str) -> Result<()> {
106 self.with_service("cache", image, Box::new(CacheServicePlugin::new(image))).await
107 }
108
109 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 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 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 self.env.register_service(plugin).await?;
125
126 let handle = self.env.start_service(service_type).await?;
128
129 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 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 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
162pub struct TestContext {
164 env: Arc<CleanroomEnvironment>,
165 services: ServiceSetup,
166}
167
168impl TestContext {
169 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 pub fn services(&self) -> &ServiceSetup {
182 &self.services
183 }
184
185 pub fn env(&self) -> &Arc<CleanroomEnvironment> {
187 &self.env
188 }
189}
190
191pub 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 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 Ok(())
232 })
233 }
234
235 fn health_check(&self, _handle: &ServiceHandle) -> HealthStatus {
236 HealthStatus::Healthy
238 }
239}
240
241pub 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
287pub 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
333pub 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
379pub async fn with_database(image: &str) -> Result<()> {
384 println!("🚀 Setting up database with image: {}", image);
387 println!("✅ Database service configured");
388 Ok(())
389}
390
391pub async fn with_cache(image: &str) -> Result<()> {
393 println!("🚀 Setting up cache with image: {}", image);
394 println!("✅ Cache service configured");
395 Ok(())
396}
397
398pub 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
405pub 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 assert!(setup.services.read().await.is_empty());
423 }
424
425
426
427 #[tokio::test]
428 async fn test_jane_friendly_functions() {
429 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}