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