rust_test_harness/
lib.rs

1use std::sync::{Arc, Mutex};
2use std::time::{Duration, Instant};
3use std::panic::{catch_unwind, AssertUnwindSafe};
4use std::collections::HashMap;
5use std::any::Any;
6use std::cell::RefCell;
7use once_cell::sync::OnceCell;
8use log::{info, warn, error};
9
10// Global shared context for before_all/after_all hooks
11static GLOBAL_SHARED_DATA: OnceCell<Arc<Mutex<HashMap<String, String>>>> = OnceCell::new();
12
13// Global container registry for automatic cleanup
14static CONTAINER_REGISTRY: OnceCell<Arc<Mutex<Vec<String>>>> = OnceCell::new();
15
16pub fn get_global_context() -> Arc<Mutex<HashMap<String, String>>> {
17    GLOBAL_SHARED_DATA.get_or_init(|| Arc::new(Mutex::new(HashMap::new()))).clone()
18}
19
20pub fn clear_global_context() {
21    if let Some(global_ctx) = GLOBAL_SHARED_DATA.get() {
22        if let Ok(mut map) = global_ctx.lock() {
23            map.clear();
24        }
25    }
26}
27
28pub fn get_container_registry() -> Arc<Mutex<Vec<String>>> {
29    CONTAINER_REGISTRY.get_or_init(|| Arc::new(Mutex::new(Vec::new()))).clone()
30}
31
32pub fn register_container_for_cleanup(container_id: &str) {
33    if let Ok(mut containers) = get_container_registry().lock() {
34        containers.push(container_id.to_string());
35        info!("๐Ÿ“ Registered container {} for automatic cleanup", container_id);
36    }
37}
38
39pub fn cleanup_all_containers() {
40    if let Ok(mut containers) = get_container_registry().lock() {
41        info!("๐Ÿงน Cleaning up {} registered containers", containers.len());
42        let container_ids: Vec<String> = containers.drain(..).collect();
43        drop(containers); // Drop the lock before processing
44        
45        // Clean up containers with timeout protection
46        for container_id in container_ids {
47            let config = ContainerConfig::new("dummy"); // dummy config for cleanup
48            
49            // Use a timeout to prevent hanging
50            let stop_result = std::panic::catch_unwind(|| {
51                // Set a reasonable timeout for container stop operations
52                let stop_future = config.stop(&container_id);
53                
54                // In a real implementation, we'd use async/await with timeout
55                // For now, we'll just attempt the stop and log any issues
56                match stop_future {
57                    Ok(_) => info!("โœ… Successfully stopped container {}", container_id),
58                    Err(e) => warn!("Failed to cleanup container {}: {}", container_id, e),
59                }
60            });
61            
62            if let Err(panic_info) = stop_result {
63                warn!("Panic while stopping container {}: {:?}", container_id, panic_info);
64            }
65        }
66    }
67}
68
69// --- Thread-local test registry ---
70// Each test thread gets its own isolated registry - no manual cleanup needed!
71
72thread_local! {
73    static THREAD_TESTS: RefCell<Vec<TestCase>> = RefCell::new(Vec::new());
74    static THREAD_BEFORE_ALL: RefCell<Vec<HookFn>> = RefCell::new(Vec::new());
75    static THREAD_BEFORE_EACH: RefCell<Vec<HookFn>> = RefCell::new(Vec::new());
76    static THREAD_AFTER_EACH: RefCell<Vec<HookFn>> = RefCell::new(Vec::new());
77    static THREAD_AFTER_ALL: RefCell<Vec<HookFn>> = RefCell::new(Vec::new());
78}
79
80// --- Test registry management ---
81// Auto-clears when run_tests() is called - no manual intervention needed
82
83pub fn clear_test_registry() {
84    // This function is now optional - mainly for manual cleanup if needed
85    THREAD_TESTS.with(|tests| tests.borrow_mut().clear());
86    THREAD_BEFORE_ALL.with(|hooks| hooks.borrow_mut().clear());
87    THREAD_BEFORE_EACH.with(|hooks| hooks.borrow_mut().clear());
88    THREAD_AFTER_EACH.with(|hooks| hooks.borrow_mut().clear());
89    THREAD_AFTER_ALL.with(|hooks| hooks.borrow_mut().clear());
90}
91
92// --- Type definitions ---
93
94pub type TestResult = Result<(), TestError>;
95pub type TestFn = Box<dyn FnOnce(&mut TestContext) -> TestResult + Send + 'static>;
96pub type HookFn = Arc<Mutex<Box<dyn FnMut(&mut TestContext) -> TestResult + Send>>>;
97
98pub struct TestCase {
99    pub name: String,
100    pub test_fn: Option<TestFn>, // Changed to Option to allow safe Send+Sync
101    pub tags: Vec<String>,
102    pub timeout: Option<Duration>,
103    pub status: TestStatus,
104}
105
106impl Clone for TestCase {
107    fn clone(&self) -> Self {
108        Self {
109            name: self.name.clone(),
110            test_fn: None, // Clone with None since we can't clone the function
111            tags: self.tags.clone(),
112            timeout: self.timeout.clone(),
113            status: self.status.clone(),
114        }
115    }
116}
117
118// TestCase is now automatically Send + Sync since test_fn is Option<TestFn>
119// and all other fields are already Send + Sync
120
121#[derive(Debug, Clone, PartialEq)]
122pub enum TestStatus {
123    Pending,
124    Running,
125    Passed,
126    Failed(TestError),
127    Skipped,
128}
129
130#[derive(Debug)]
131pub struct TestContext {
132    pub docker_handle: Option<DockerHandle>,
133    pub start_time: Instant,
134    pub data: HashMap<String, Box<dyn Any + Send + Sync>>,
135}
136
137impl TestContext {
138    pub fn new() -> Self {
139        Self {
140            docker_handle: None,
141            start_time: Instant::now(),
142            data: HashMap::new(),
143        }
144    }
145    
146    /// Store arbitrary data in the test context
147    pub fn set_data<T: Any + Send + Sync>(&mut self, key: &str, value: T) {
148        self.data.insert(key.to_string(), Box::new(value));
149    }
150    
151    /// Retrieve data from the test context
152    pub fn get_data<T: Any + Send + Sync>(&self, key: &str) -> Option<&T> {
153        self.data.get(key).and_then(|boxed| boxed.downcast_ref::<T>())
154    }
155    
156    /// Check if data exists in the test context
157    pub fn has_data(&self, key: &str) -> bool {
158        self.data.contains_key(key)
159    }
160    
161    /// Remove data from the test context
162    pub fn remove_data<T: Any + Send + Sync>(&mut self, key: &str) -> Option<T> {
163        self.data.remove(key).and_then(|boxed| {
164            match boxed.downcast::<T>() {
165                Ok(value) => Some(*value),
166                Err(_) => None,
167            }
168        })
169    }
170    
171    // Removed get_global_data function - it was a footgun that never worked
172    // Use get_data() instead, which properly accesses data set by before_all hooks
173}
174
175impl Clone for TestContext {
176    fn clone(&self) -> Self {
177        Self {
178            docker_handle: self.docker_handle.clone(),
179            start_time: self.start_time,
180            data: HashMap::new(), // Can't clone Box<dyn Any>, start fresh
181        }
182    }
183}
184
185#[derive(Debug, Clone)]
186pub struct DockerHandle {
187    pub container_id: String,
188    pub ports: Vec<(u16, u16)>, // (host_port, container_port)
189}
190
191
192
193#[derive(Debug, Clone)]
194pub struct TestConfig {
195    pub filter: Option<String>,
196    pub skip_tags: Vec<String>,
197    pub max_concurrency: Option<usize>,
198    pub shuffle_seed: Option<u64>,
199    pub color: Option<bool>,
200    pub html_report: Option<String>,
201    pub skip_hooks: Option<bool>,
202    pub timeout_config: TimeoutConfig,
203}
204
205impl Default for TestConfig {
206    fn default() -> Self {
207        Self {
208            filter: std::env::var("TEST_FILTER").ok(),
209            skip_tags: std::env::var("TEST_SKIP_TAGS")
210                .map(|s| s.split(',').map(|s| s.trim().to_string()).collect())
211                .unwrap_or_default(),
212            max_concurrency: std::env::var("TEST_MAX_CONCURRENCY")
213                .ok()
214                .and_then(|s| s.parse().ok()),
215            shuffle_seed: std::env::var("TEST_SHUFFLE_SEED")
216                .ok()
217                .and_then(|s| s.parse().ok()),
218            color: Some(atty::is(atty::Stream::Stdout)),
219            html_report: std::env::var("TEST_HTML_REPORT").ok(),
220            skip_hooks: std::env::var("TEST_SKIP_HOOKS")
221                .ok()
222                .and_then(|s| s.parse().ok()),
223            timeout_config: TimeoutConfig::default(),
224        }
225    }
226}
227
228// --- Global test registration functions ---
229// Users just call these - no runners needed!
230
231pub fn before_all<F>(f: F) 
232where 
233    F: FnMut(&mut TestContext) -> TestResult + Send + 'static 
234{
235    THREAD_BEFORE_ALL.with(|hooks| hooks.borrow_mut().push(Arc::new(Mutex::new(Box::new(f)))));
236}
237
238pub fn before_each<F>(f: F) 
239where 
240    F: FnMut(&mut TestContext) -> TestResult + Send + 'static 
241{
242    THREAD_BEFORE_EACH.with(|hooks| hooks.borrow_mut().push(Arc::new(Mutex::new(Box::new(f)))));
243}
244
245pub fn after_each<F>(f: F) 
246where 
247    F: FnMut(&mut TestContext) -> TestResult + Send + 'static 
248{
249    THREAD_AFTER_EACH.with(|hooks| hooks.borrow_mut().push(Arc::new(Mutex::new(Box::new(f)))));
250}
251
252pub fn after_all<F>(f: F) 
253where 
254    F: FnMut(&mut TestContext) -> TestResult + Send + 'static 
255{
256    THREAD_AFTER_ALL.with(|hooks| hooks.borrow_mut().push(Arc::new(Mutex::new(Box::new(f)))));
257}
258
259pub fn test<F>(name: &str, f: F) 
260where 
261    F: FnMut(&mut TestContext) -> TestResult + Send + 'static 
262{
263    THREAD_TESTS.with(|tests| tests.borrow_mut().push(TestCase {
264        name: name.to_string(),
265        test_fn: Some(Box::new(f)),
266        tags: Vec::new(),
267        timeout: None,
268        status: TestStatus::Pending,
269    }));
270}
271
272
273
274pub fn test_with_tags<F>(name: &'static str, tags: Vec<&'static str>, f: F) 
275where 
276    F: FnMut(&mut TestContext) -> TestResult + Send + 'static 
277{
278    THREAD_TESTS.with(|tests| tests.borrow_mut().push(TestCase {
279        name: name.to_string(),
280        test_fn: Some(Box::new(f)),
281        tags: tags.into_iter().map(|s| s.to_string()).collect(),
282        timeout: None,
283        status: TestStatus::Pending,
284    }));
285}
286
287
288
289pub fn test_with_timeout<F>(name: &str, timeout: Duration, f: F) 
290where 
291    F: FnMut(&mut TestContext) -> TestResult + Send + 'static 
292{
293    THREAD_TESTS.with(|tests| tests.borrow_mut().push(TestCase {
294        name: name.to_string(),
295        test_fn: Some(Box::new(f)),
296        tags: Vec::new(),
297        timeout: Some(timeout),
298        status: TestStatus::Pending,
299    }));
300}
301
302// --- Main execution function ---
303// Users just call this to run all registered tests in parallel!
304
305pub fn run_tests() -> i32 {
306    let config = TestConfig::default();
307    run_tests_with_config(config)
308}
309
310pub fn run_tests_with_config(config: TestConfig) -> i32 {
311    let start_time = Instant::now();
312    
313    info!("๐Ÿš€ Starting test execution with config: {:?}", config);
314    
315    // Get all tests and hooks from thread-local storage
316    let mut tests = THREAD_TESTS.with(|t| t.borrow_mut().drain(..).collect::<Vec<_>>());
317    let before_all_hooks = THREAD_BEFORE_ALL.with(|h| h.borrow_mut().drain(..).collect::<Vec<_>>());
318    let before_each_hooks = THREAD_BEFORE_EACH.with(|h| h.borrow_mut().drain(..).collect::<Vec<_>>());
319    let after_each_hooks = THREAD_AFTER_EACH.with(|h| h.borrow_mut().drain(..).collect::<Vec<_>>());
320    let after_all_hooks = THREAD_AFTER_ALL.with(|h| h.borrow_mut().drain(..).collect::<Vec<_>>());
321    
322    info!("๐Ÿ“‹ Found {} tests to run", tests.len());
323    
324    if tests.is_empty() {
325        warn!("โš ๏ธ  No tests registered to run");
326        return 0;
327    }
328    
329    // Run before_all hooks ONCE at the beginning
330    let mut shared_context = TestContext::new();
331    if !config.skip_hooks.unwrap_or(false) && !before_all_hooks.is_empty() {
332        info!("๐Ÿ”„ Running {} before_all hooks", before_all_hooks.len());
333        
334        // Execute each before_all hook with the shared context
335        for hook in before_all_hooks {
336            // Wrap hook execution with panic safety
337            let result = catch_unwind(AssertUnwindSafe(|| {
338                if let Ok(mut hook_fn) = hook.lock() {
339                    hook_fn(&mut shared_context)
340                } else {
341                    Err(TestError::Message("Failed to acquire hook lock".into()))
342                }
343            }));
344            match result {
345                Ok(Ok(())) => {
346                    // Hook succeeded
347                }
348                Ok(Err(e)) => {
349                    error!("โŒ before_all hook failed: {}", e);
350                    return 1; // Fail the entire test run
351                }
352                Err(panic_info) => {
353                    let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
354                        s.to_string()
355                    } else if let Some(s) = panic_info.downcast_ref::<String>() {
356                        s.clone()
357                    } else {
358                        "unknown panic".to_string()
359                    };
360                    error!("๐Ÿ’ฅ before_all hook panicked: {}", panic_msg);
361                    return 1; // Fail the entire test run
362                }
363            }
364        }
365        
366        info!("โœ… before_all hooks completed");
367        
368        // Copy data from shared context to global context for individual tests
369        let global_ctx = get_global_context();
370        clear_global_context(); // Clear any existing data
371        for (key, value) in &shared_context.data {
372            if let Some(string_value) = value.downcast_ref::<String>() {
373                if let Ok(mut map) = global_ctx.lock() {
374                    map.insert(key.clone(), string_value.clone());
375                }
376            }
377        }
378    }
379    
380    // Filter and sort tests
381    let test_indices = filter_and_sort_test_indices(&tests, &config);
382    let filtered_count = test_indices.len();
383    
384    if filtered_count == 0 {
385        warn!("โš ๏ธ  No tests match the current filter");
386        return 0;
387    }
388    
389    info!("๐ŸŽฏ Running {} filtered tests", filtered_count);
390    
391    let mut overall_failed = 0usize;
392    let mut overall_skipped = 0usize;
393    
394    // Run tests in parallel or sequential based on config
395    if let Some(max_concurrency) = config.max_concurrency {
396        if max_concurrency > 1 {
397            info!("โšก Running tests in parallel with max concurrency: {}", max_concurrency);
398            run_tests_parallel_by_index(&mut tests, &test_indices, before_each_hooks, after_each_hooks, &config, &mut overall_failed, &mut overall_skipped, &mut shared_context);
399        } else {
400            info!("๐ŸŒ Running tests sequentially (max_concurrency = 1)");
401            run_tests_sequential_by_index(&mut tests, &test_indices, before_each_hooks, after_each_hooks, &config, &mut overall_failed, &mut overall_skipped, &mut shared_context);
402        }
403    } else {
404        // Default to parallel execution
405        let default_concurrency = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4);
406        info!("โšก Running tests in parallel with default concurrency: {}", default_concurrency);
407        run_tests_parallel_by_index(&mut tests, &test_indices, before_each_hooks, after_each_hooks, &config, &mut overall_failed, &mut overall_skipped, &mut shared_context);
408    }
409    
410
411    
412    // Run after_all hooks
413    if !config.skip_hooks.unwrap_or(false) && !after_all_hooks.is_empty() {
414        info!("๐Ÿ”„ Running {} after_all hooks", after_all_hooks.len());
415        
416        // Execute each after_all hook with the same shared context
417        for hook in after_all_hooks {
418            // Wrap hook execution with panic safety
419            let result = catch_unwind(AssertUnwindSafe(|| {
420                if let Ok(mut hook_fn) = hook.lock() {
421                    hook_fn(&mut shared_context)
422                } else {
423                    Err(TestError::Message("Failed to acquire hook lock".into()))
424                }
425            }));
426            match result {
427                Ok(Ok(())) => {
428                    // Hook succeeded
429                }
430                Ok(Err(e)) => {
431                    warn!("โš ๏ธ  after_all hook failed: {}", e);
432                    // Don't fail the entire test run for after_all hook failures
433                }
434                Err(panic_info) => {
435                    let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
436                        s.to_string()
437                    } else if let Some(s) = panic_info.downcast_ref::<String>() {
438                        s.clone()
439                    } else {
440                        "unknown panic".to_string()
441                    };
442                    warn!("๐Ÿ’ฅ after_all hook panicked: {}", panic_msg);
443                    // Don't fail the entire test run for after_all hook panics
444                }
445            }
446        }
447        
448        info!("โœ… after_all hooks completed");
449    }
450    
451    let total_time = start_time.elapsed();
452    
453    // Print summary
454    let passed = tests.iter().filter(|t| matches!(t.status, TestStatus::Passed)).count();
455    let failed = tests.iter().filter(|t| matches!(t.status, TestStatus::Failed(_))).count();
456    let skipped = tests.iter().filter(|t| matches!(t.status, TestStatus::Skipped)).count();
457    
458    info!("\n๐Ÿ“Š TEST EXECUTION SUMMARY");
459    info!("==========================");
460    info!("Total tests: {}", tests.len());
461    info!("Passed: {}", passed);
462    info!("Failed: {}", failed);
463    info!("Skipped: {}", skipped);
464    info!("Total time: {:?}", total_time);
465    
466    // Generate HTML report if requested
467    if let Some(ref html_path) = config.html_report {
468        if let Err(e) = generate_html_report(&tests, total_time, html_path) {
469            warn!("โš ๏ธ  Failed to generate HTML report: {}", e);
470        } else {
471            info!("๐Ÿ“Š HTML report generated: {}", html_path);
472        }
473    }
474    
475    if failed > 0 {
476        error!("\nโŒ FAILED TESTS:");
477        for test in tests.iter().filter(|t| matches!(t.status, TestStatus::Failed(_))) {
478            if let TestStatus::Failed(error) = &test.status {
479                error!("  {}: {}", test.name, error);
480            }
481        }
482    }
483    
484    // Clean up any remaining containers
485    cleanup_all_containers();
486    
487    if failed > 0 {
488        error!("โŒ Test execution failed with {} failures", failed);
489        1
490    } else {
491        info!("โœ… All tests passed!");
492        0
493    }
494}
495
496// --- Helper functions ---
497
498fn filter_and_sort_test_indices(tests: &[TestCase], config: &TestConfig) -> Vec<usize> {
499    let mut indices: Vec<usize> = (0..tests.len()).collect();
500    
501    // Apply filter
502    if let Some(ref filter) = config.filter {
503        indices.retain(|&idx| tests[idx].name.contains(filter));
504    }
505    
506    // Apply tag filtering
507    if !config.skip_tags.is_empty() {
508        indices.retain(|&idx| {
509            let test_tags = &tests[idx].tags;
510            !config.skip_tags.iter().any(|skip_tag| test_tags.contains(skip_tag))
511        });
512    }
513    
514    // Apply shuffling using Fisher-Yates algorithm with seeded PRNG
515    if let Some(seed) = config.shuffle_seed {
516        use std::collections::hash_map::DefaultHasher;
517        use std::hash::{Hash, Hasher};
518        
519        // Create a simple seeded PRNG using the hash
520        let mut hasher = DefaultHasher::new();
521        seed.hash(&mut hasher);
522        let mut rng_state = hasher.finish();
523        
524        // Fisher-Yates shuffle
525        for i in (1..indices.len()).rev() {
526            // Generate next pseudo-random number
527            rng_state = rng_state.wrapping_mul(1103515245).wrapping_add(12345);
528            let j = (rng_state as usize) % (i + 1);
529            indices.swap(i, j);
530        }
531    }
532    
533    indices
534}
535
536fn run_tests_parallel_by_index(
537    tests: &mut [TestCase],
538    test_indices: &[usize],
539    before_each_hooks: Vec<HookFn>,
540    after_each_hooks: Vec<HookFn>,
541    config: &TestConfig,
542    overall_failed: &mut usize,
543    overall_skipped: &mut usize,
544    _shared_context: &mut TestContext,
545) {
546    let max_workers = config.max_concurrency.unwrap_or_else(|| {
547        std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4)
548    });
549    
550    info!("Running {} tests in parallel with {} workers", test_indices.len(), max_workers);
551    
552    // Use rayon for true parallel execution
553    use rayon::prelude::*;
554    
555
556    
557    // Create a thread pool with the specified concurrency
558    let pool = rayon::ThreadPoolBuilder::new()
559        .num_threads(max_workers)
560        .build()
561        .expect("Failed to create thread pool");
562    
563
564    
565    // Extract test functions and create test data before parallel execution to avoid borrowing issues
566    let mut test_functions: Vec<Arc<Mutex<TestFn>>> = Vec::new();
567    let mut test_data: Vec<(String, Vec<String>, Option<Duration>, TestStatus)> = Vec::new();
568    
569    for idx in test_indices {
570        let test_fn = std::mem::replace(&mut tests[*idx].test_fn, None).unwrap_or_else(|| Box::new(|_| Ok(())));
571        test_functions.push(Arc::new(Mutex::new(test_fn)));
572        
573        // Extract all the data we need from the test
574        let test = &tests[*idx];
575        test_data.push((
576            test.name.clone(),
577            test.tags.clone(),
578            test.timeout.clone(),
579            test.status.clone(),
580        ));
581    }
582    
583    // Collect results from parallel execution
584    let results: Vec<_> = pool.install(|| {
585        test_indices.par_iter().enumerate().map(|(i, &idx)| {
586            // Create a new test from the extracted data
587            let (name, tags, timeout, status) = &test_data[i];
588            let mut test = TestCase {
589                name: name.clone(),
590                test_fn: None, // Will be set to None since we extracted the function
591                tags: tags.clone(),
592                timeout: *timeout,
593                status: status.clone(),
594            };
595            
596            let test_fn = test_functions[i].clone();
597            
598            // Clone hooks for this thread
599            let before_hooks = before_each_hooks.clone();
600            let after_hooks = after_each_hooks.clone();
601            
602            // Run the test in parallel with the extracted function
603            run_single_test_by_index_parallel_with_fn(
604                &mut test,
605                test_fn,
606                &before_hooks,
607                &after_hooks,
608                config,
609            );
610            
611            (idx, test)
612        }).collect()
613    });
614    
615    // Update the original test array with results
616    for (idx, test_result) in results {
617        tests[idx] = test_result;
618        
619        // Update counters
620        match &tests[idx].status {
621            TestStatus::Failed(_) => *overall_failed += 1,
622            TestStatus::Skipped => *overall_skipped += 1,
623            _ => {}
624        }
625    }
626}
627
628fn run_tests_sequential_by_index(
629    tests: &mut [TestCase],
630    test_indices: &[usize],
631    mut before_each_hooks: Vec<HookFn>,
632    mut after_each_hooks: Vec<HookFn>,
633    config: &TestConfig,
634    overall_failed: &mut usize,
635    overall_skipped: &mut usize,
636    shared_context: &mut TestContext,
637) {
638    for &idx in test_indices {
639        run_single_test_by_index(
640            tests,
641            idx,
642            &mut before_each_hooks,
643            &mut after_each_hooks,
644            config,
645            overall_failed,
646            overall_skipped,
647            shared_context,
648        );
649    }
650}
651
652fn run_single_test_by_index(
653    tests: &mut [TestCase],
654    idx: usize,
655    before_each_hooks: &mut [HookFn],
656    after_each_hooks: &mut [HookFn],
657    config: &TestConfig,
658    overall_failed: &mut usize,
659    overall_skipped: &mut usize,
660    _shared_context: &mut TestContext,
661) {
662    let test = &mut tests[idx];
663    let test_name = &test.name;
664    
665    info!("๐Ÿงช Running test: {}", test_name);
666    
667    // Check if test should be skipped
668    if let Some(ref filter) = config.filter {
669        if !test_name.contains(filter) {
670            test.status = TestStatus::Skipped;
671            *overall_skipped += 1;
672            info!("โญ๏ธ  Test '{}' skipped (filter: {})", test_name, filter);
673            return;
674        }
675    }
676    
677    // Check tag filtering
678    if !config.skip_tags.is_empty() {
679        let test_tags = &test.tags;
680        if config.skip_tags.iter().any(|skip_tag| test_tags.contains(skip_tag)) {
681            test.status = TestStatus::Skipped;
682            *overall_skipped += 1;
683            info!("โญ๏ธ  Test '{}' skipped (tags: {:?})", test_name, test_tags);
684            return;
685        }
686    }
687    
688    test.status = TestStatus::Running;
689    let start_time = Instant::now();
690    
691    // Create test context
692    let mut ctx = TestContext::new();
693    
694    // Copy data from global context to test context
695    // This allows tests to access data set by before_all hooks
696    let global_ctx = get_global_context();
697    if let Ok(map) = global_ctx.lock() {
698        for (key, value) in map.iter() {
699            ctx.set_data(key, value.clone());
700        }
701    }
702    
703    // Run before_each hooks
704    if !config.skip_hooks.unwrap_or(false) {
705        for hook in before_each_hooks.iter_mut() {
706            // Wrap hook execution with panic safety
707            let result = catch_unwind(AssertUnwindSafe(|| {
708                if let Ok(mut hook_fn) = hook.lock() {
709                    hook_fn(&mut ctx)
710                } else {
711                    Err(TestError::Message("Failed to acquire hook lock".into()))
712                }
713            }));
714            match result {
715                Ok(Ok(())) => {
716                    // Hook succeeded
717                }
718                Ok(Err(e)) => {
719                    error!("โŒ before_each hook failed: {}", e);
720                    test.status = TestStatus::Failed(e.clone());
721                    *overall_failed += 1;
722                    return;
723                }
724                Err(panic_info) => {
725                    let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
726                        s.to_string()
727                    } else if let Some(s) = panic_info.downcast_ref::<String>() {
728                        s.clone()
729                    } else {
730                        "unknown panic".to_string()
731                    };
732                    error!("๐Ÿ’ฅ before_each hook panicked: {}", panic_msg);
733                    test.status = TestStatus::Failed(TestError::Panicked(panic_msg));
734                    *overall_failed += 1;
735                    return;
736                }
737            }
738        }
739    }
740    
741    // Run the test
742    let test_result = if let Some(timeout) = test.timeout {
743        let test_fn = std::mem::replace(&mut test.test_fn, None).unwrap_or_else(|| Box::new(|_| Ok(())));
744        run_test_with_timeout(test_fn, &mut ctx, timeout)
745    } else {
746        let test_fn = std::mem::replace(&mut test.test_fn, None).unwrap_or_else(|| Box::new(|_| Ok(())));
747        run_test(test_fn, &mut ctx)
748    };
749    
750    // Run after_each hooks
751    if !config.skip_hooks.unwrap_or(false) {
752        for hook in after_each_hooks.iter_mut() {
753            // Wrap hook execution with panic safety
754            let result = catch_unwind(AssertUnwindSafe(|| {
755                if let Ok(mut hook_fn) = hook.lock() {
756                    hook_fn(&mut ctx)
757                } else {
758                    Err(TestError::Message("Failed to acquire hook lock".into()))
759                }
760            }));
761            match result {
762                Ok(Ok(())) => {
763                    // Hook succeeded
764                }
765                Ok(Err(e)) => {
766                    warn!("โš ๏ธ  after_each hook failed: {}", e);
767                    // Don't fail the test for after_each hook failures
768                }
769                Err(panic_info) => {
770                    let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
771                        s.to_string()
772                    } else if let Some(s) = panic_info.downcast_ref::<String>() {
773                        s.clone()
774                    } else {
775                        "unknown panic".to_string()
776                    };
777                    warn!("๐Ÿ’ฅ after_each hook panicked: {}", panic_msg);
778                    // Don't fail the test for after_each hook panics
779                }
780            }
781        }
782    }
783    
784    let elapsed = start_time.elapsed();
785    
786    match test_result {
787        Ok(()) => {
788            test.status = TestStatus::Passed;
789            info!("โœ… Test '{}' passed in {:?}", test_name, elapsed);
790        }
791        Err(e) => {
792            test.status = TestStatus::Failed(e.clone());
793            *overall_failed += 1;
794            error!("โŒ Test '{}' failed in {:?}: {}", test_name, elapsed, e);
795        }
796    }
797    
798    // Clean up Docker if used
799    if let Some(ref docker_handle) = ctx.docker_handle {
800        cleanup_docker_container(docker_handle);
801    }
802}
803
804fn run_single_test_by_index_parallel_with_fn(
805    test: &mut TestCase,
806    test_fn: Arc<Mutex<TestFn>>,
807    before_each_hooks: &[HookFn],
808    after_each_hooks: &[HookFn],
809    config: &TestConfig,
810) {
811    let test_name = &test.name;
812    
813    info!("๐Ÿงช Running test: {}", test_name);
814    
815    // Check if test should be skipped
816    if let Some(ref filter) = config.filter {
817        if !test_name.contains(filter) {
818            test.status = TestStatus::Skipped;
819            info!("โญ๏ธ  Test '{}' skipped (filter: {})", test_name, filter);
820            return;
821        }
822    }
823    
824    // Check tag filtering
825    if !config.skip_tags.is_empty() {
826        let test_tags = &test.tags;
827        if config.skip_tags.iter().any(|skip_tag| test_tags.contains(skip_tag)) {
828            test.status = TestStatus::Skipped;
829            info!("โญ๏ธ  Test '{}' skipped (tags: {:?})", test_name, test_tags);
830            return;
831        }
832    }
833    
834    let start_time = Instant::now();
835    
836    // Create test context
837    let mut ctx = TestContext::new();
838    // Copy data from global context to test context
839    // This allows tests to access data set by before_all hooks
840    let global_ctx = get_global_context();
841    if let Ok(map) = global_ctx.lock() {
842        for (key, value) in map.iter() {
843            ctx.set_data(key, value.clone());
844        }
845    }
846    
847    // Run before_each hooks
848    if !config.skip_hooks.unwrap_or(false) {
849        for hook in before_each_hooks.iter() {
850            // Wrap hook execution with panic safety
851            let result = catch_unwind(AssertUnwindSafe(|| {
852                if let Ok(mut hook_fn) = hook.lock() {
853                    hook_fn(&mut ctx)
854                } else {
855                    Err(TestError::Message("Failed to acquire hook lock".into()))
856                }
857            }));
858            match result {
859                Ok(Ok(())) => {
860                    // Hook succeeded
861                }
862                Ok(Err(e)) => {
863                    error!("โŒ before_each hook failed: {}", e);
864                    test.status = TestStatus::Failed(e.clone());
865                    return;
866                }
867                Err(panic_info) => {
868                    let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
869                        s.to_string()
870                    } else if let Some(s) = panic_info.downcast_ref::<String>() {
871                        s.clone()
872                    } else {
873                        "unknown panic".to_string()
874                    };
875                    error!("๐Ÿ’ฅ before_each hook panicked: {}", panic_msg);
876                    test.status = TestStatus::Failed(TestError::Panicked(panic_msg));
877                    return;
878                }
879            }
880        }
881    }
882    
883    // Run the test
884    let test_result = if let Some(timeout) = test.timeout {
885        if let Ok(mut fn_box) = test_fn.lock() {
886            let test_fn = std::mem::replace(&mut *fn_box, Box::new(|_| Ok(())));
887            run_test_with_timeout(test_fn, &mut ctx, timeout)
888        } else {
889            Err(TestError::Message("Failed to acquire test function lock".into()))
890        }
891    } else {
892        if let Ok(mut fn_box) = test_fn.lock() {
893            let test_fn = std::mem::replace(&mut *fn_box, Box::new(|_| Ok(())));
894            run_test(test_fn, &mut ctx)
895        } else {
896            Err(TestError::Message("Failed to acquire test function lock".into()))
897        }
898    };
899    
900    // Run after_each hooks
901    if !config.skip_hooks.unwrap_or(false) {
902        for hook in after_each_hooks.iter() {
903            // Wrap hook execution with panic safety
904            let result = catch_unwind(AssertUnwindSafe(|| {
905                if let Ok(mut hook_fn) = hook.lock() {
906                    hook_fn(&mut ctx)
907                } else {
908                    Err(TestError::Message("Failed to acquire hook lock".into()))
909                }
910            }));
911            match result {
912                Ok(Ok(())) => {
913                    // Hook succeeded
914                }
915                Ok(Err(e)) => {
916                    warn!("โš ๏ธ  after_each hook failed: {}", e);
917                    // Don't fail the test for after_each hook failures
918                }
919                Err(panic_info) => {
920                    let panic_msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
921                        s.to_string()
922                    } else if let Some(s) = panic_info.downcast_ref::<String>() {
923                        s.clone()
924                    } else {
925                        "unknown panic".to_string()
926                    };
927                    warn!("๐Ÿ’ฅ after_each hook panicked: {}", panic_msg);
928                    // Don't fail the test for after_each hook panics
929                }
930            }
931        }
932    }
933    
934    let elapsed = start_time.elapsed();
935    
936    match test_result {
937        Ok(()) => {
938            test.status = TestStatus::Passed;
939            info!("โœ… Test '{}' passed in {:?}", test_name, elapsed);
940        }
941        Err(e) => {
942            test.status = TestStatus::Failed(e.clone());
943            error!("โŒ Test '{}' failed in {:?}: {}", test_name, elapsed, e);
944        }
945    }
946    
947    // Clean up Docker if used
948    if let Some(ref docker_handle) = ctx.docker_handle {
949        cleanup_docker_container(docker_handle);
950    }
951}
952
953fn run_test<F>(test_fn: F, ctx: &mut TestContext) -> TestResult 
954where 
955    F: FnOnce(&mut TestContext) -> TestResult
956{
957    catch_unwind(AssertUnwindSafe(|| test_fn(ctx))).unwrap_or_else(|panic_info| {
958        let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
959            s.to_string()
960        } else if let Some(s) = panic_info.downcast_ref::<String>() {
961            s.clone()
962        } else {
963            "unknown panic".to_string()
964        };
965        Err(TestError::Panicked(msg))
966    })
967}
968
969fn run_test_with_timeout<F>(test_fn: F, ctx: &mut TestContext, timeout: Duration) -> TestResult 
970where 
971    F: FnOnce(&mut TestContext) -> TestResult + Send + 'static
972{
973    // Use the enhanced timeout with configurable strategies
974    run_test_with_timeout_enhanced(test_fn, ctx, timeout, &TimeoutConfig::default())
975}
976
977fn run_test_with_timeout_enhanced<F>(
978    test_fn: F, 
979    ctx: &mut TestContext, 
980    timeout: Duration, 
981    config: &TimeoutConfig
982) -> TestResult 
983where 
984    F: FnOnce(&mut TestContext) -> TestResult + Send + 'static
985{
986    use std::sync::mpsc;
987    
988    let (tx, rx) = mpsc::channel();
989    
990    // Spawn test in worker thread with a new context
991    let handle = std::thread::spawn(move || {
992        let mut worker_ctx = TestContext::new();
993        let result = catch_unwind(AssertUnwindSafe(|| test_fn(&mut worker_ctx)));
994        let _ = tx.send((result, worker_ctx));
995    });
996    
997    // Wait for result with timeout based on strategy
998    let recv_result = match config.strategy {
999        TimeoutStrategy::Simple => {
1000            // Simple strategy - just wait for the full timeout
1001            rx.recv_timeout(timeout)
1002        }
1003        TimeoutStrategy::Aggressive => {
1004            // Aggressive strategy - interrupt immediately on timeout
1005            rx.recv_timeout(timeout)
1006        }
1007        TimeoutStrategy::Graceful(cleanup_time) => {
1008            // Graceful strategy - allow cleanup time
1009            let main_timeout = timeout.saturating_sub(cleanup_time);
1010            match rx.recv_timeout(main_timeout) {
1011                Ok(result) => Ok(result),
1012                Err(mpsc::RecvTimeoutError::Timeout) => {
1013                    // Give cleanup time, then force timeout
1014                    match rx.recv_timeout(cleanup_time) {
1015                        Ok(result) => Ok(result),
1016                        Err(_) => Err(mpsc::RecvTimeoutError::Timeout),
1017                    }
1018                }
1019                Err(e) => Err(e),
1020            }
1021        }
1022    };
1023    
1024    match recv_result {
1025        Ok((Ok(test_result), worker_ctx)) => {
1026            // Test completed without panic
1027            match test_result {
1028                Ok(()) => {
1029                    // Test passed - copy any data changes back to original context
1030                    for (key, value) in &worker_ctx.data {
1031                        if let Some(string_value) = value.downcast_ref::<String>() {
1032                            ctx.set_data(key, string_value.clone());
1033                        }
1034                    }
1035                    Ok(())
1036                }
1037                Err(e) => {
1038                    // Test failed with error
1039                    Err(e)
1040                }
1041            }
1042        }
1043        Ok((Err(panic_info), _)) => {
1044            // Test panicked
1045            let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
1046                s.to_string()
1047            } else if let Some(s) = panic_info.downcast_ref::<String>() {
1048                s.clone()
1049            } else {
1050                "unknown panic".to_string()
1051            };
1052            Err(TestError::Panicked(msg))
1053        }
1054        Err(mpsc::RecvTimeoutError::Timeout) => {
1055            // Test timed out - handle based on strategy
1056            match config.strategy {
1057                TimeoutStrategy::Simple => {
1058                    warn!("  โš ๏ธ  Test took longer than {:?} (Simple strategy)", timeout);
1059                    Err(TestError::Timeout(timeout))
1060                }
1061                TimeoutStrategy::Aggressive => {
1062                    warn!("  โš ๏ธ  Test timed out after {:?} - interrupting", timeout);
1063                    drop(handle); // This will join the thread when it goes out of scope
1064                    Err(TestError::Timeout(timeout))
1065                }
1066                TimeoutStrategy::Graceful(_) => {
1067                    warn!("  โš ๏ธ  Test timed out after {:?} - graceful cleanup attempted", timeout);
1068                    drop(handle);
1069                    Err(TestError::Timeout(timeout))
1070                }
1071            }
1072        }
1073        Err(mpsc::RecvTimeoutError::Disconnected) => {
1074            // Worker thread error
1075            Err(TestError::Message("worker thread error".into()))
1076        }
1077    }
1078}
1079
1080
1081fn cleanup_docker_container(handle: &DockerHandle) {
1082    info!("๐Ÿงน Cleaning up Docker container: {}", handle.container_id);
1083    // In a real implementation, this would use the Docker API to stop and remove the container
1084    // For now, just log the cleanup
1085}
1086
1087// --- Error types ---
1088
1089#[derive(Debug, Clone, PartialEq)]
1090pub enum TestError {
1091    Message(String),
1092    Panicked(String),
1093    Timeout(Duration),
1094}
1095
1096impl std::fmt::Display for TestError {
1097    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1098        match self {
1099            TestError::Message(msg) => write!(f, "{}", msg),
1100            TestError::Panicked(msg) => write!(f, "panicked: {}", msg),
1101                    TestError::Timeout(duration) => write!(f, "timeout after {:?}", duration),
1102        }
1103    }
1104}
1105
1106impl From<&str> for TestError {
1107    fn from(s: &str) -> Self {
1108        TestError::Message(s.to_string())
1109    }
1110}
1111
1112impl From<String> for TestError {
1113    fn from(s: String) -> Self {
1114        TestError::Message(s)
1115    }
1116}
1117
1118#[derive(Debug, Clone, PartialEq, Eq)]
1119pub enum TimeoutStrategy {
1120    /// Simple timeout - just report when exceeded
1121    Simple,
1122    /// Aggressive timeout - attempt to interrupt the test
1123    Aggressive,
1124    /// Graceful timeout - allow cleanup before interruption
1125    Graceful(Duration),
1126}
1127
1128impl Default for TimeoutStrategy {
1129    fn default() -> Self {
1130        TimeoutStrategy::Aggressive
1131    }
1132}
1133
1134#[derive(Debug, Clone, PartialEq, Eq)]
1135pub struct TimeoutConfig {
1136    pub strategy: TimeoutStrategy,
1137}
1138
1139impl Default for TimeoutConfig {
1140    fn default() -> Self {
1141        Self {
1142            strategy: TimeoutStrategy::default(),
1143        }
1144    }
1145}
1146
1147// --- HTML Report Generation ---
1148
1149fn generate_html_report(tests: &[TestCase], total_time: Duration, output_path: &str) -> Result<(), Box<dyn std::error::Error>> {
1150    info!("๐Ÿ”ง generate_html_report called with {} tests, duration: {:?}, output: {}", tests.len(), total_time, output_path);
1151    
1152    // Ensure the target directory exists and create the full path
1153    let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| "target".to_string());
1154    let html_dir = format!("{}/test-reports", target_dir);
1155    info!("๐Ÿ“ Creating directory: {}", html_dir);
1156    std::fs::create_dir_all(&html_dir)?;
1157    info!("โœ… Directory created/verified: {}", html_dir);
1158    
1159    // Determine the final path - if output_path is absolute, use it directly; otherwise place in target/test-reports/
1160    let final_path = if std::path::Path::new(output_path).is_absolute() {
1161        output_path.to_string()
1162    } else {
1163        // Extract just the filename from the path and place it in target/test-reports/
1164        let filename = std::path::Path::new(output_path)
1165            .file_name()
1166            .and_then(|name| name.to_str())
1167            .unwrap_or("test-report.html");
1168        format!("{}/{}", html_dir, filename)
1169    };
1170    info!("๐Ÿ“„ Final HTML path: {}", final_path);
1171    
1172    let mut html = String::new();
1173    
1174    // HTML header
1175    html.push_str(r#"<!DOCTYPE html>
1176<html lang="en">
1177<head>
1178    <meta charset="UTF-8">
1179    <meta name="viewport" content="width=device-width, initial-scale=1.0">
1180    <title>Test Execution Report</title>
1181    <style>
1182        body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
1183        .container { max-width: 1200px; margin: 0 auto; background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; }
1184        .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; text-align: center; }
1185        .header h1 { margin: 0; font-size: 2.5em; font-weight: 300; }
1186        .header .subtitle { margin: 10px 0 0 0; opacity: 0.9; font-size: 1.1em; }
1187        .summary { padding: 30px; border-bottom: 1px solid #eee; }
1188        .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; }
1189        .summary-card { background: #f8f9fa; padding: 20px; border-radius: 6px; text-align: center; border-left: 4px solid #007bff; }
1190        .summary-card.passed { border-left-color: #28a745; }
1191        .summary-card.failed { border-left-color: #dc3545; }
1192        .summary-card.skipped { border-left-color: #ffc107; }
1193        .summary-card .number { font-size: 2em; font-weight: bold; margin-bottom: 5px; }
1194        .summary-card .label { color: #6c757d; font-size: 0.9em; text-transform: uppercase; letter-spacing: 0.5px; }
1195        .tests-section { padding: 30px; }
1196        .tests-section h2 { margin: 0 0 20px 0; color: #333; }
1197        .test-list { display: grid; gap: 15px; }
1198        .test-item { background: #f8f9fa; border-radius: 6px; padding: 15px; border-left: 4px solid #dee2e6; transition: all 0.2s ease; }
1199        .test-item:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); }
1200        .test-item.passed { border-left-color: #28a745; background: #f8fff9; }
1201        .test-item.failed { border-left-color: #dc3545; background: #fff8f8; }
1202        .test-item.skipped { border-left-color: #ffc107; background: #fffef8; }
1203        .test-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; cursor: pointer; }
1204        .test-name { font-weight: 600; color: #333; }
1205        .test-status { padding: 4px 12px; border-radius: 20px; font-size: 0.8em; font-weight: 600; text-transform: uppercase; }
1206        .test-status.passed { background: #d4edda; color: #155724; }
1207        .test-status.failed { background: #f8d7da; color: #721c24; }
1208        .test-status.skipped { background: #fff3cd; color: #856404; }
1209        .test-details { font-size: 0.9em; color: #6c757d; }
1210        .test-error { background: #f8d7da; color: #721c24; padding: 10px; border-radius: 4px; margin-top: 10px; font-family: monospace; font-size: 0.85em; }
1211        .test-expandable { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-in-out; }
1212        .test-expandable.expanded { max-height: 500px; }
1213        .expand-icon { transition: transform 0.2s ease; font-size: 1.2em; color: #6c757d; }
1214        .expand-icon.expanded { transform: rotate(90deg); }
1215        .test-metadata { background: #f1f3f4; padding: 15px; border-radius: 6px; margin-top: 10px; }
1216        .metadata-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; }
1217        .metadata-item { display: flex; flex-direction: column; }
1218        .metadata-label { font-weight: 600; color: #495057; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 5px; }
1219        .metadata-value { color: #6c757d; font-size: 0.9em; }
1220        .footer { background: #f8f9fa; padding: 20px; text-align: center; color: #6c757d; font-size: 0.9em; border-top: 1px solid #eee; }
1221        .timestamp { color: #007bff; }
1222        .filters { background: #e9ecef; padding: 15px; border-radius: 6px; margin: 20px 0; font-size: 0.9em; }
1223        .filters strong { color: #495057; }
1224        .search-box { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; margin-bottom: 20px; font-size: 1em; }
1225        .search-box:focus { outline: none; border-color: #007bff; box-shadow: 0 0 0 2px rgba(0,123,255,0.25); }
1226        .test-item.hidden { display: none; }
1227        .no-results { text-align: center; padding: 40px; color: #6c757d; font-style: italic; }
1228        @media (max-width: 768px) { .summary-grid { grid-template-columns: 1fr; } .test-header { flex-direction: column; align-items: flex-start; gap: 10px; } .metadata-grid { grid-template-columns: 1fr; } }
1229    </style>
1230</head>
1231<body>
1232    <div class="container">
1233        <div class="header">
1234            <h1>๐Ÿงช Test Execution Report</h1>
1235            <p class="subtitle">Comprehensive test results and analysis</p>
1236        </div>
1237        
1238        <div class="summary">
1239            <h2>๐Ÿ“Š Execution Summary</h2>
1240            <div class="summary-grid">"#);
1241    
1242    // Summary statistics
1243    let passed = tests.iter().filter(|t| matches!(t.status, TestStatus::Passed)).count();
1244    let failed = tests.iter().filter(|t| matches!(t.status, TestStatus::Failed(_))).count();
1245    let skipped = tests.iter().filter(|t| matches!(t.status, TestStatus::Skipped)).count();
1246    
1247    html.push_str(&format!(r#"
1248                <div class="summary-card passed">
1249                    <div class="number">{}</div>
1250                    <div class="label">Passed</div>
1251                </div>
1252                <div class="summary-card failed">
1253                    <div class="number">{}</div>
1254                    <div class="label">Failed</div>
1255                </div>
1256                <div class="summary-card skipped">
1257                    <div class="number">{}</div>
1258                    <div class="label">Skipped</div>
1259                </div>
1260                <div class="summary-card">
1261                    <div class="number">{}</div>
1262                    <div class="label">Total</div>
1263                </div>
1264            </div>
1265            <p><strong>Total Execution Time:</strong> <span class="timestamp">{:?}</span></p>
1266        </div>
1267        
1268        <div class="tests-section">
1269            <h2>๐Ÿ“Š Test Results</h2>
1270            
1271            <input type="text" class="search-box" id="testSearch" placeholder="๐Ÿ” Search tests by name, status, or tags..." />
1272            
1273            <div class="test-list" id="testList">"#, passed, failed, skipped, tests.len(), total_time));
1274    
1275    // Test results
1276    for test in tests {
1277        let status_class = match test.status {
1278            TestStatus::Passed => "passed",
1279            TestStatus::Failed(_) => "failed",
1280            TestStatus::Skipped => "skipped",
1281            TestStatus::Pending => "skipped",
1282            TestStatus::Running => "skipped",
1283        };
1284        
1285        let status_text = match test.status {
1286            TestStatus::Passed => "PASSED",
1287            TestStatus::Failed(_) => "FAILED",
1288            TestStatus::Skipped => "SKIPPED",
1289            TestStatus::Pending => "PENDING",
1290            TestStatus::Running => "RUNNING",
1291        };
1292        
1293        html.push_str(&format!(r#"
1294                <div class="test-item {}" data-test-name="{}" data-test-status="{}" data-test-tags="{}">
1295                    <div class="test-header" onclick="toggleTestDetails(this)">
1296                        <div class="test-name">{}</div>
1297                        <div style="display: flex; align-items: center; gap: 10px;">
1298                            <div class="test-status {}">{}</div>
1299                            <span class="expand-icon">โ–ถ</span>
1300                        </div>
1301                    </div>
1302                    
1303                    <div class="test-expandable">
1304                        <div class="test-metadata">
1305                            <div class="metadata-grid">"#, 
1306            status_class, test.name, status_text, test.tags.join(","), test.name, status_class, status_text));
1307        
1308        // Add test metadata
1309        if !test.tags.is_empty() {
1310            html.push_str(&format!(r#"<div class="metadata-item"><div class="metadata-label">Tags</div><div class="metadata-value">{}</div></div>"#, test.tags.join(", ")));
1311        }
1312        
1313        if let Some(timeout) = test.timeout {
1314            html.push_str(&format!(r#"<div class="metadata-item"><div class="metadata-label">Timeout</div><div class="metadata-value">{:?}</div></div>"#, timeout));
1315        }
1316        
1317
1318        
1319        html.push_str(r#"</div></div>"#);
1320        
1321        // Add error details for failed tests
1322        if let TestStatus::Failed(error) = &test.status {
1323            html.push_str(&format!(r#"<div class="test-error"><strong>Error:</strong> {}</div>"#, error));
1324        }
1325        
1326        html.push_str("</div></div>");
1327    }
1328    
1329    // HTML footer
1330    html.push_str(r#"
1331            </div>
1332        </div>
1333        
1334        <div class="footer">
1335            <p>Report generated by <strong>rust-test-harness</strong> at <span class="timestamp">"#);
1336    
1337    html.push_str(&chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string());
1338    
1339    html.push_str(r#"</span></p>
1340        </div>
1341    </div>
1342    
1343    <script>
1344        // Expandable test details functionality
1345        function toggleTestDetails(header) {
1346            const testItem = header.closest('.test-item');
1347            const expandable = testItem.querySelector('.test-expandable');
1348            const expandIcon = header.querySelector('.expand-icon');
1349            
1350            if (expandable.classList.contains('expanded')) {
1351                expandable.classList.remove('expanded');
1352                expandIcon.classList.remove('expanded');
1353                expandIcon.textContent = 'โ–ถ';
1354            } else {
1355                expandable.classList.add('expanded');
1356                expandIcon.classList.add('expanded');
1357                expandIcon.textContent = 'โ–ผ';
1358            }
1359        }
1360        
1361        // Search functionality
1362        document.getElementById('testSearch').addEventListener('input', function(e) {
1363            const searchTerm = e.target.value.toLowerCase();
1364            const testItems = document.querySelectorAll('.test-item');
1365            let visibleCount = 0;
1366            
1367            testItems.forEach(item => {
1368                const testName = item.getAttribute('data-test-name').toLowerCase();
1369                const testStatus = item.getAttribute('data-test-status').toLowerCase();
1370                const testTags = item.getAttribute('data-test-tags').toLowerCase();
1371                
1372                const matches = testName.includes(searchTerm) || 
1373                               testStatus.includes(searchTerm) || 
1374                               testTags.includes(searchTerm);
1375                
1376                if (matches) {
1377                    item.classList.remove('hidden');
1378                    visibleCount++;
1379                } else {
1380                    item.classList.add('hidden');
1381                }
1382            });
1383            
1384            // Show/hide no results message
1385            const noResults = document.querySelector('.no-results');
1386            if (visibleCount === 0 && searchTerm.length > 0) {
1387                if (!noResults) {
1388                    const message = document.createElement('div');
1389                    message.className = 'no-results';
1390                    message.textContent = 'No tests match your search criteria';
1391                    document.getElementById('testList').appendChild(message);
1392                }
1393            } else if (noResults) {
1394                noResults.remove();
1395            }
1396        });
1397        
1398        // Keyboard shortcuts
1399        document.addEventListener('keydown', function(e) {
1400            if (e.ctrlKey || e.metaKey) {
1401                switch(e.key) {
1402                    case 'f':
1403                        e.preventDefault();
1404                        document.getElementById('testSearch').focus();
1405                        break;
1406                    case 'a':
1407                        e.preventDefault();
1408                        // Expand all test details
1409                        document.querySelectorAll('.test-expandable').forEach(expandable => {
1410                            expandable.classList.add('expanded');
1411                        });
1412                        document.querySelectorAll('.expand-icon').forEach(icon => {
1413                            icon.classList.add('expanded');
1414                            icon.textContent = 'โ–ผ';
1415                        });
1416                        break;
1417                    case 'z':
1418                        e.preventDefault();
1419                        // Collapse all test details
1420                        document.querySelectorAll('.test-expandable').forEach(expandable => {
1421                            expandable.classList.remove('expanded');
1422                        });
1423                        document.querySelectorAll('.expand-icon').forEach(icon => {
1424                            icon.classList.remove('expanded');
1425                            icon.textContent = 'โ–ถ';
1426                        });
1427                        break;
1428                }
1429            }
1430        });
1431        
1432        // Auto-expand failed tests for better visibility
1433        document.addEventListener('DOMContentLoaded', function() {
1434            const failedTests = document.querySelectorAll('.test-item.failed');
1435            failedTests.forEach(testItem => {
1436                const expandable = testItem.querySelector('.test-expandable');
1437                const expandIcon = testItem.querySelector('.expand-icon');
1438                if (expandable && expandIcon) {
1439                    expandable.classList.add('expanded');
1440                    expandIcon.classList.add('expanded');
1441                    expandIcon.textContent = 'โ–ผ';
1442                }
1443            });
1444        });
1445    </script>
1446</body>
1447</html>"#);
1448    
1449    // Write to file in target/test-reports directory
1450    std::fs::write(&final_path, html)?;
1451    
1452    // Log the actual file location for user convenience
1453    info!("๐Ÿ“„ HTML report written to: {}", final_path);
1454    
1455    Ok(())
1456}
1457
1458// --- Macros ---
1459
1460/// Macro to create individual test functions that can be run independently
1461/// This makes the framework compatible with cargo test and existing test libraries
1462/// Note: Hooks are only executed when using the main test runner, not individual macros
1463#[macro_export]
1464macro_rules! test_function {
1465    ($name:ident, $test_fn:expr) => {
1466        #[test]
1467        fn $name() {
1468            // Initialize logging for individual test runs
1469            let _ = env_logger::try_init();
1470            
1471            // Run the test function
1472            let result = ($test_fn)(&mut rust_test_harness::TestContext::new());
1473            
1474            // Convert result to test outcome
1475            match result {
1476                Ok(_) => {
1477                    // Test passed - no need to panic
1478                }
1479                Err(e) => {
1480                    panic!("โŒ Test '{}' failed: {:?}", stringify!($name), e);
1481                }
1482            }
1483        }
1484    };
1485}
1486
1487/// Macro to create individual test functions with custom names
1488/// Note: Hooks are only executed when using the main test runner, not individual macros
1489#[macro_export]
1490macro_rules! test_named {
1491    ($name:expr, $test_fn:expr) => {
1492        #[test]
1493        fn test_named_function() {
1494            // Initialize logging for individual test runs
1495            let _ = env_logger::try_init();
1496            
1497            // Run the test function
1498            let result = ($test_fn)(&mut rust_test_harness::TestContext::new());
1499            
1500            // Convert result to test outcome
1501            match result {
1502                Ok(_) => {
1503                    // Test passed - no need to panic
1504                }
1505                Err(e) => {
1506                    panic!("โŒ Test '{}' failed: {:?}", $name, e);
1507                }
1508            }
1509        }
1510    };
1511}
1512
1513/// Macro to create individual async test functions (for when you add async support)
1514/// Note: Hooks are only executed when using the main test runner, not individual macros
1515#[macro_export]
1516macro_rules! test_async {
1517    ($name:ident, $test_fn:expr) => {
1518        #[tokio::test]
1519        async fn $name() {
1520            // Initialize logging for individual test runs
1521            let _ = env_logger::try_init();
1522            
1523            // Run the async test function
1524            let result = ($test_fn)(&mut rust_test_harness::TestContext::new()).await;
1525            
1526            // Convert result to test outcome
1527            match result {
1528                Ok(_) => {
1529                    // Test passed - no need to panic
1530                }
1531                Err(e) => {
1532                    panic!("โŒ Async test '{}' failed: {:?}", stringify!($name), e);
1533                }
1534            }
1535        }
1536    };
1537}
1538
1539/// Macro to create test cases that work exactly like Rust's built-in #[test] attribute
1540/// but with our framework's enhanced features (hooks, Docker, etc.)
1541/// 
1542/// **IDE Support**: This macro creates a standard #[test] function that RustRover will recognize.
1543/// 
1544/// Usage:
1545/// ```rust
1546/// #[cfg(test)]
1547/// mod tests {
1548///     use super::*;
1549///     use rust_test_harness::test_case;
1550///     
1551///     test_case!(test_something, |ctx| {
1552///         // Your test logic here
1553///         assert_eq!(2 + 2, 4);
1554///         Ok(())
1555///     });
1556///     
1557///     test_case!(test_with_docker, |ctx| {
1558///         // Test with Docker context
1559///         Ok(())
1560///     });
1561/// }
1562/// ```
1563#[macro_export]
1564macro_rules! test_case {
1565    ($name:ident, $test_fn:expr) => {
1566        #[test]
1567        #[allow(unused_imports)]
1568        fn $name() {
1569            // Initialize logging for individual test runs
1570            let _ = env_logger::try_init();
1571            
1572            // Run the test function
1573            let result: rust_test_harness::TestResult = ($test_fn)(&mut rust_test_harness::TestContext::new());
1574            
1575            // Convert result to test outcome
1576            match result {
1577                Ok(_) => {
1578                    // Test passed - no need to panic
1579                }
1580                Err(e) => {
1581                    panic!("Test failed: {:?}", e);
1582                }
1583            }
1584        }
1585    };
1586}
1587
1588/// Macro to create test cases with custom names (useful for dynamic test names)
1589/// 
1590/// Usage:
1591/// ```rust
1592/// #[cfg(test)]
1593/// mod tests {
1594///     use super::*;
1595///     use rust_test_harness::test_case_named;
1596///     
1597///     test_case_named!(my_custom_test_name, |ctx| {
1598///         // Your test logic here
1599///         Ok(())
1600///     });
1601/// }
1602/// ```
1603#[macro_export]
1604macro_rules! test_case_named {
1605    ($name:ident, $test_fn:expr) => {
1606        #[test]
1607        fn $name() {
1608            // Initialize logging for individual test runs
1609            let _ = env_logger::try_init();
1610            
1611            // Run the test function
1612            let result: rust_test_harness::TestResult = ($test_fn)(&mut rust_test_harness::TestContext::new());
1613            
1614            // Convert result to test outcome
1615            match result {
1616                Ok(_) => {
1617                    // Test passed - no need to panic
1618                }
1619                Err(e) => {
1620                    panic!("Test '{}' failed: {:?}", stringify!($name), e);
1621                }
1622            }
1623        }
1624    };
1625}
1626
1627
1628
1629#[derive(Debug, Clone)]
1630pub struct ContainerConfig {
1631    pub image: String,
1632    pub ports: Vec<(u16, u16)>, // (host_port, container_port)
1633    pub auto_ports: Vec<u16>, // container ports that should get auto-assigned host ports
1634    pub env: Vec<(String, String)>,
1635    pub name: Option<String>,
1636    pub ready_timeout: Duration,
1637    pub auto_cleanup: bool, // automatically cleanup on drop/test end
1638}
1639
1640#[derive(Debug, Clone)]
1641pub struct ContainerInfo {
1642    pub container_id: String,
1643    pub image: String,
1644    pub name: Option<String>,
1645    pub urls: Vec<String>, // URLs for all exposed ports
1646    pub port_mappings: Vec<(u16, u16)>, // (host_port, container_port) for all ports
1647    pub auto_cleanup: bool,
1648}
1649
1650impl ContainerInfo {
1651    /// Get the primary URL (first port)
1652    pub fn primary_url(&self) -> Option<&str> {
1653        self.urls.first().map(|s| s.as_str())
1654    }
1655    
1656    /// Get host:port for a specific container port
1657    pub fn url_for_port(&self, container_port: u16) -> Option<String> {
1658        self.port_mappings.iter()
1659            .find(|(_, cp)| *cp == container_port)
1660            .map(|(host_port, _)| format!("localhost:{}", host_port))
1661    }
1662    
1663    /// Get host port for a specific container port
1664    pub fn host_port_for(&self, container_port: u16) -> Option<u16> {
1665        self.port_mappings.iter()
1666            .find(|(_, cp)| *cp == container_port)
1667            .map(|(host_port, _)| *host_port)
1668    }
1669    
1670    /// Get all exposed ports as a formatted string
1671    pub fn ports_summary(&self) -> String {
1672        if self.port_mappings.is_empty() {
1673            "No ports exposed".to_string()
1674        } else {
1675            self.port_mappings.iter()
1676                .map(|(host_port, container_port)| format!("{}->{}", host_port, container_port))
1677                .collect::<Vec<_>>()
1678                .join(", ")
1679        }
1680    }
1681}
1682
1683impl ContainerConfig {
1684    pub fn new(image: &str) -> Self {
1685        Self {
1686            image: image.to_string(),
1687            ports: Vec::new(),
1688            auto_ports: Vec::new(),
1689            env: Vec::new(),
1690            name: None,
1691            ready_timeout: Duration::from_secs(30),
1692            auto_cleanup: true, // enable auto-cleanup by default
1693        }
1694    }
1695    
1696    pub fn port(mut self, host_port: u16, container_port: u16) -> Self {
1697        self.ports.push((host_port, container_port));
1698        self
1699    }
1700    
1701    pub fn env(mut self, key: &str, value: &str) -> Self {
1702        self.env.push((key.to_string(), value.to_string()));
1703        self
1704    }
1705    
1706    pub fn name(mut self, name: &str) -> Self {
1707        self.name = Some(name.to_string());
1708        self
1709    }
1710    
1711    pub fn ready_timeout(mut self, timeout: Duration) -> Self {
1712        self.ready_timeout = timeout;
1713        self
1714    }
1715    
1716    /// Add a port that should be automatically assigned an available host port
1717    pub fn auto_port(mut self, container_port: u16) -> Self {
1718        self.auto_ports.push(container_port);
1719        self
1720    }
1721    
1722    /// Disable automatic cleanup (containers will persist after tests)
1723    pub fn no_auto_cleanup(mut self) -> Self {
1724        self.auto_cleanup = false;
1725        self
1726    }
1727    
1728    /// Find an available port on the host
1729    fn find_available_port() -> Result<u16, Box<dyn std::error::Error + Send + Sync>> {
1730        use std::net::TcpListener;
1731        
1732        // Try to bind to port 0 to let the OS assign an available port
1733        let listener = TcpListener::bind("127.0.0.1:0")?;
1734        let addr = listener.local_addr()?;
1735        Ok(addr.port())
1736    }
1737    
1738    /// Start a container with this configuration using Docker API
1739    pub fn start(&self) -> Result<ContainerInfo, Box<dyn std::error::Error + Send + Sync>> {
1740        // Real Docker API implementation - spawn Tokio runtime for async operations
1741        let runtime = tokio::runtime::Runtime::new()
1742            .map_err(|e| format!("Failed to create Tokio runtime: {}", e))?;
1743        
1744        let result = runtime.block_on(async {
1745            use bollard::Docker;
1746            use bollard::models::{ContainerCreateBody, HostConfig, PortBinding, PortMap};
1747            
1748            // Connect to Docker daemon
1749            let docker = Docker::connect_with_local_defaults()
1750                .map_err(|e| format!("Failed to connect to Docker: {}", e))?;
1751            
1752            // Build port bindings - handle both manual and auto-ports
1753            let mut port_bindings = PortMap::new();
1754            let mut auto_port_mappings = Vec::new();
1755            
1756            // Handle manual port mappings
1757            for (host_port, container_port) in &self.ports {
1758                let binding = vec![PortBinding {
1759                    host_ip: Some("127.0.0.1".to_string()),
1760                    host_port: Some(host_port.to_string()),
1761                }];
1762                port_bindings.insert(format!("{}/tcp", container_port), Some(binding));
1763            }
1764            
1765            // Handle auto-ports - find available host ports
1766            for container_port in &self.auto_ports {
1767                let host_port = Self::find_available_port()
1768                    .map_err(|e| format!("Failed to find available port: {}", e))?;
1769                
1770                let binding = vec![PortBinding {
1771                    host_ip: Some("127.0.0.1".to_string()),
1772                    host_port: Some(host_port.to_string()),
1773                }];
1774                port_bindings.insert(format!("{}/tcp", container_port), Some(binding));
1775                
1776                // Store the mapping for return
1777                auto_port_mappings.push((host_port, *container_port));
1778            }
1779            
1780            // Build environment variables
1781            let env_vars: Vec<String> = self.env.iter()
1782                .map(|(k, v)| format!("{}={}", k, v))
1783                .collect();
1784            
1785            // Create container configuration using the correct bollard 0.19 API
1786            // For alpine, busybox, and ubuntu images, add a command to keep them running
1787            let cmd = if self.image.contains("alpine") || self.image.contains("busybox") || self.image.contains("ubuntu") {
1788                Some(vec!["sleep".to_string(), "3600".to_string()]) // Sleep for 1 hour
1789            } else {
1790                None
1791            };
1792            
1793            let container_config = ContainerCreateBody {
1794                image: Some(self.image.clone()),
1795                env: Some(env_vars),
1796                cmd,
1797                host_config: Some(HostConfig {
1798                    port_bindings: Some(port_bindings),
1799                    ..Default::default()
1800                }),
1801                ..Default::default()
1802            };
1803            
1804            // Create the container
1805            let container = docker.create_container(None::<bollard::query_parameters::CreateContainerOptions>, container_config)
1806                .await
1807                .map_err(|e| format!("Failed to create container: {}", e))?;
1808            let id = container.id;
1809            
1810            // Start the container
1811            docker.start_container(&id, None::<bollard::query_parameters::StartContainerOptions>)
1812                .await
1813                .map_err(|e| format!("Failed to start container: {}", e))?;
1814            
1815            // Wait for container to be ready
1816            self.wait_for_ready_async(&docker, &id).await?;
1817            
1818            // Build port mappings and URLs
1819            let mut all_port_mappings = self.ports.clone();
1820            all_port_mappings.extend(auto_port_mappings);
1821            
1822            let urls: Vec<String> = all_port_mappings.iter()
1823                .map(|(host_port, _)| format!("http://localhost:{}", host_port))
1824                .collect();
1825            
1826            let container_info = ContainerInfo {
1827                container_id: id.clone(),
1828                image: self.image.clone(),
1829                name: self.name.clone(),
1830                urls,
1831                port_mappings: all_port_mappings,
1832                auto_cleanup: self.auto_cleanup,
1833            };
1834            
1835            Ok::<ContainerInfo, Box<dyn std::error::Error + Send + Sync>>(container_info)
1836        });
1837        
1838        match result {
1839            Ok(container_info) => {
1840                info!("๐Ÿš€ Started Docker container {} with image {}", container_info.container_id, self.image);
1841                
1842                // Register for auto-cleanup if enabled
1843                if container_info.auto_cleanup {
1844                    register_container_for_cleanup(&container_info.container_id);
1845                }
1846                
1847                // Log port information
1848                if !container_info.port_mappings.is_empty() {
1849                    info!("๐ŸŒ Container {} exposed on ports:", container_info.container_id);
1850                    for (host_port, container_port) in &container_info.port_mappings {
1851                        info!("   {} -> {} (http://localhost:{})", host_port, container_port, host_port);
1852                    }
1853                }
1854                
1855                Ok(container_info)
1856            }
1857            Err(e) => Err(e),
1858        }
1859    }
1860    
1861    /// Stop a container by ID using Docker API
1862    pub fn stop(&self, container_id: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1863        // Real Docker API implementation - spawn Tokio runtime for async operations
1864        let runtime = tokio::runtime::Runtime::new()
1865            .map_err(|e| format!("Failed to create Tokio runtime: {}", e))?;
1866        
1867        let result = runtime.block_on(async {
1868            use bollard::Docker;
1869            use tokio::time::{timeout, Duration as TokioDuration};
1870            
1871            // Connect to Docker (synchronous in bollard 0.19)
1872            let docker = Docker::connect_with_local_defaults()
1873                .map_err(|e| format!("Failed to connect to Docker: {}", e))?;
1874            
1875            // Stop the container with timeout (ignore errors for non-existent containers)
1876            let stop_result = timeout(
1877                TokioDuration::from_secs(10), // 10 second timeout for stop
1878                docker.stop_container(container_id, None::<bollard::query_parameters::StopContainerOptions>)
1879            ).await;
1880            
1881            match stop_result {
1882                Ok(Ok(())) => info!("๐Ÿ›‘ Container {} stopped successfully", container_id),
1883                Ok(Err(e)) => {
1884                    let error_msg = e.to_string();
1885                    if error_msg.contains("No such container") || error_msg.contains("not found") {
1886                        info!("โ„น๏ธ Container {} already removed or doesn't exist", container_id);
1887                    } else {
1888                        warn!("Failed to stop container {}: {}", container_id, e);
1889                        // Don't return error for cleanup operations - just log and continue
1890                    }
1891                },
1892                Err(_) => {
1893                    warn!("Container stop timeout for {}", container_id);
1894                    // Don't return error for cleanup operations - just log and continue
1895                },
1896            }
1897            
1898            // Remove the container with timeout (ignore errors for non-existent containers)
1899            let remove_result = timeout(
1900                TokioDuration::from_secs(10), // 10 second timeout for remove
1901                docker.remove_container(container_id, None::<bollard::query_parameters::RemoveContainerOptions>)
1902            ).await;
1903            
1904            match remove_result {
1905                Ok(Ok(())) => info!("๐Ÿ—‘๏ธ Container {} removed successfully", container_id),
1906                Ok(Err(e)) => {
1907                    let error_msg = e.to_string();
1908                    if error_msg.contains("No such container") || error_msg.contains("not found") {
1909                        info!("โ„น๏ธ Container {} already removed or doesn't exist", container_id);
1910                    } else {
1911                        warn!("Failed to remove container {}: {}", container_id, e);
1912                        // Don't return error for cleanup operations - just log and continue
1913                    }
1914                },
1915                Err(_) => {
1916                    warn!("Container remove timeout for {}", container_id);
1917                    // Don't return error for cleanup operations - just log and continue
1918                },
1919            }
1920            
1921            Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
1922        });
1923        
1924        match result {
1925            Ok(()) => {
1926                info!("๐Ÿ›‘ Stopped and removed Docker container {}", container_id);
1927                Ok(())
1928            }
1929            Err(e) => Err(e),
1930        }
1931    }
1932    
1933    async fn wait_for_ready_async(&self, docker: &bollard::Docker, container_id: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1934        use tokio::time::{sleep, Duration as TokioDuration};
1935        
1936        // Wait for container to be ready by checking its status
1937        let start_time = std::time::Instant::now();
1938        let timeout = self.ready_timeout;
1939        
1940        loop {
1941            if start_time.elapsed() > timeout {
1942                return Err("Container readiness timeout".into());
1943            }
1944            
1945            // Inspect container to check status
1946            let inspect_result = docker.inspect_container(container_id, None::<bollard::query_parameters::InspectContainerOptions>).await;
1947            if let Ok(container_info) = inspect_result {
1948                if let Some(state) = container_info.state {
1949                    if let Some(running) = state.running {
1950                        if running {
1951                            if let Some(health) = state.health {
1952                                if let Some(status) = health.status {
1953                                    if status.to_string() == "healthy" {
1954                                        info!("โœ… Container {} is healthy and ready", container_id);
1955                                        return Ok(());
1956                                    }
1957                                }
1958                            } else {
1959                                // No health check, assume ready if running
1960                                info!("โœ… Container {} is running and ready", container_id);
1961                                return Ok(());
1962                            }
1963                        }
1964                    }
1965                }
1966            }
1967            
1968            // Wait a bit before checking again
1969            sleep(TokioDuration::from_millis(500)).await;
1970        }
1971    }
1972}
1973
1974// --- Hook execution functions for individual tests ---
1975
1976/// Execute before_all hooks for individual test functions
1977pub fn execute_before_all_hooks() -> Result<(), TestError> {
1978    THREAD_BEFORE_ALL.with(|hooks| {
1979        let mut hooks = hooks.borrow_mut();
1980        for hook in hooks.iter_mut() {
1981            if let Ok(mut hook_fn) = hook.lock() {
1982                hook_fn(&mut TestContext::new())?;
1983            }
1984        }
1985        Ok(())
1986    })
1987}
1988
1989/// Execute before_each hooks for individual test functions
1990pub fn execute_before_each_hooks() -> Result<(), TestError> {
1991    THREAD_BEFORE_EACH.with(|hooks| {
1992        let mut hooks = hooks.borrow_mut();
1993        for hook in hooks.iter_mut() {
1994            if let Ok(mut hook_fn) = hook.lock() {
1995                hook_fn(&mut TestContext::new())?;
1996            }
1997        }
1998        Ok(())
1999    })
2000}
2001
2002/// Execute after_each hooks for individual test functions
2003pub fn execute_after_each_hooks() -> Result<(), TestError> {
2004    THREAD_AFTER_EACH.with(|hooks| {
2005        let mut hooks = hooks.borrow_mut();
2006        for hook in hooks.iter_mut() {
2007            if let Ok(mut hook_fn) = hook.lock() {
2008                let _ = hook_fn(&mut TestContext::new());
2009            }
2010        }
2011        Ok(())
2012    })
2013}
2014
2015/// Execute after_all hooks for individual test functions
2016pub fn execute_after_all_hooks() -> Result<(), TestError> {
2017    THREAD_AFTER_ALL.with(|hooks| {
2018        let mut hooks = hooks.borrow_mut();
2019        for hook in hooks.iter_mut() {
2020            if let Ok(mut hook_fn) = hook.lock() {
2021                let _ = hook_fn(&mut TestContext::new());
2022            }
2023        }
2024        Ok(())
2025    })
2026}
2027
2028// --- Convenience function for running tests ---
2029
2030pub fn run_all() -> i32 {
2031    run_tests()
2032}
2033
2034