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
10static GLOBAL_SHARED_DATA: OnceCell<Arc<Mutex<HashMap<String, String>>>> = OnceCell::new();
12
13static 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); for container_id in container_ids {
47 let config = ContainerConfig::new("dummy"); let stop_result = std::panic::catch_unwind(|| {
51 let stop_future = config.stop(&container_id);
53
54 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
69thread_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
80pub fn clear_test_registry() {
84 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
92pub 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>, 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, tags: self.tags.clone(),
112 timeout: self.timeout.clone(),
113 status: self.status.clone(),
114 }
115 }
116}
117
118#[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 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 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 pub fn has_data(&self, key: &str) -> bool {
158 self.data.contains_key(key)
159 }
160
161 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 }
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(), }
182 }
183}
184
185#[derive(Debug, Clone)]
186pub struct DockerHandle {
187 pub container_id: String,
188 pub ports: Vec<(u16, u16)>, }
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
228pub 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
302pub 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 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 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 for hook in before_all_hooks {
336 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 }
348 Ok(Err(e)) => {
349 error!("โ before_all hook failed: {}", e);
350 return 1; }
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; }
363 }
364 }
365
366 info!("โ
before_all hooks completed");
367
368 let global_ctx = get_global_context();
370 clear_global_context(); 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 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 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 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 if !config.skip_hooks.unwrap_or(false) && !after_all_hooks.is_empty() {
414 info!("๐ Running {} after_all hooks", after_all_hooks.len());
415
416 for hook in after_all_hooks {
418 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 }
430 Ok(Err(e)) => {
431 warn!("โ ๏ธ after_all hook failed: {}", e);
432 }
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 }
445 }
446 }
447
448 info!("โ
after_all hooks completed");
449 }
450
451 let total_time = start_time.elapsed();
452
453 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 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 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
496fn filter_and_sort_test_indices(tests: &[TestCase], config: &TestConfig) -> Vec<usize> {
499 let mut indices: Vec<usize> = (0..tests.len()).collect();
500
501 if let Some(ref filter) = config.filter {
503 indices.retain(|&idx| tests[idx].name.contains(filter));
504 }
505
506 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 if let Some(seed) = config.shuffle_seed {
516 use std::collections::hash_map::DefaultHasher;
517 use std::hash::{Hash, Hasher};
518
519 let mut hasher = DefaultHasher::new();
521 seed.hash(&mut hasher);
522 let mut rng_state = hasher.finish();
523
524 for i in (1..indices.len()).rev() {
526 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::prelude::*;
554
555
556
557 let pool = rayon::ThreadPoolBuilder::new()
559 .num_threads(max_workers)
560 .build()
561 .expect("Failed to create thread pool");
562
563
564
565 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 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 let results: Vec<_> = pool.install(|| {
585 test_indices.par_iter().enumerate().map(|(i, &idx)| {
586 let (name, tags, timeout, status) = &test_data[i];
588 let mut test = TestCase {
589 name: name.clone(),
590 test_fn: None, tags: tags.clone(),
592 timeout: *timeout,
593 status: status.clone(),
594 };
595
596 let test_fn = test_functions[i].clone();
597
598 let before_hooks = before_each_hooks.clone();
600 let after_hooks = after_each_hooks.clone();
601
602 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 for (idx, test_result) in results {
617 tests[idx] = test_result;
618
619 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 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 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 let mut ctx = TestContext::new();
693
694 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 if !config.skip_hooks.unwrap_or(false) {
705 for hook in before_each_hooks.iter_mut() {
706 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 }
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 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 if !config.skip_hooks.unwrap_or(false) {
752 for hook in after_each_hooks.iter_mut() {
753 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 }
765 Ok(Err(e)) => {
766 warn!("โ ๏ธ after_each hook failed: {}", e);
767 }
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 }
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 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 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 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 let mut ctx = TestContext::new();
838 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 if !config.skip_hooks.unwrap_or(false) {
849 for hook in before_each_hooks.iter() {
850 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 }
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 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 if !config.skip_hooks.unwrap_or(false) {
902 for hook in after_each_hooks.iter() {
903 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 }
915 Ok(Err(e)) => {
916 warn!("โ ๏ธ after_each hook failed: {}", e);
917 }
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 }
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 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 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 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 let recv_result = match config.strategy {
999 TimeoutStrategy::Simple => {
1000 rx.recv_timeout(timeout)
1002 }
1003 TimeoutStrategy::Aggressive => {
1004 rx.recv_timeout(timeout)
1006 }
1007 TimeoutStrategy::Graceful(cleanup_time) => {
1008 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 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 match test_result {
1028 Ok(()) => {
1029 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 Err(e)
1040 }
1041 }
1042 }
1043 Ok((Err(panic_info), _)) => {
1044 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 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); 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 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 }
1086
1087#[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,
1122 Aggressive,
1124 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
1147fn 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 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 let final_path = if std::path::Path::new(output_path).is_absolute() {
1161 output_path.to_string()
1162 } else {
1163 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.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 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 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 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 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.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 std::fs::write(&final_path, html)?;
1451
1452 info!("๐ HTML report written to: {}", final_path);
1454
1455 Ok(())
1456}
1457
1458#[macro_export]
1464macro_rules! test_function {
1465 ($name:ident, $test_fn:expr) => {
1466 #[test]
1467 fn $name() {
1468 let _ = env_logger::try_init();
1470
1471 let result = ($test_fn)(&mut rust_test_harness::TestContext::new());
1473
1474 match result {
1476 Ok(_) => {
1477 }
1479 Err(e) => {
1480 panic!("โ Test '{}' failed: {:?}", stringify!($name), e);
1481 }
1482 }
1483 }
1484 };
1485}
1486
1487#[macro_export]
1490macro_rules! test_named {
1491 ($name:expr, $test_fn:expr) => {
1492 #[test]
1493 fn test_named_function() {
1494 let _ = env_logger::try_init();
1496
1497 let result = ($test_fn)(&mut rust_test_harness::TestContext::new());
1499
1500 match result {
1502 Ok(_) => {
1503 }
1505 Err(e) => {
1506 panic!("โ Test '{}' failed: {:?}", $name, e);
1507 }
1508 }
1509 }
1510 };
1511}
1512
1513#[macro_export]
1516macro_rules! test_async {
1517 ($name:ident, $test_fn:expr) => {
1518 #[tokio::test]
1519 async fn $name() {
1520 let _ = env_logger::try_init();
1522
1523 let result = ($test_fn)(&mut rust_test_harness::TestContext::new()).await;
1525
1526 match result {
1528 Ok(_) => {
1529 }
1531 Err(e) => {
1532 panic!("โ Async test '{}' failed: {:?}", stringify!($name), e);
1533 }
1534 }
1535 }
1536 };
1537}
1538
1539#[macro_export]
1564macro_rules! test_case {
1565 ($name:ident, $test_fn:expr) => {
1566 #[test]
1567 #[allow(unused_imports)]
1568 fn $name() {
1569 let _ = env_logger::try_init();
1571
1572 let result: rust_test_harness::TestResult = ($test_fn)(&mut rust_test_harness::TestContext::new());
1574
1575 match result {
1577 Ok(_) => {
1578 }
1580 Err(e) => {
1581 panic!("Test failed: {:?}", e);
1582 }
1583 }
1584 }
1585 };
1586}
1587
1588#[macro_export]
1604macro_rules! test_case_named {
1605 ($name:ident, $test_fn:expr) => {
1606 #[test]
1607 fn $name() {
1608 let _ = env_logger::try_init();
1610
1611 let result: rust_test_harness::TestResult = ($test_fn)(&mut rust_test_harness::TestContext::new());
1613
1614 match result {
1616 Ok(_) => {
1617 }
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)>, pub auto_ports: Vec<u16>, pub env: Vec<(String, String)>,
1635 pub name: Option<String>,
1636 pub ready_timeout: Duration,
1637 pub auto_cleanup: bool, }
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>, pub port_mappings: Vec<(u16, u16)>, pub auto_cleanup: bool,
1648}
1649
1650impl ContainerInfo {
1651 pub fn primary_url(&self) -> Option<&str> {
1653 self.urls.first().map(|s| s.as_str())
1654 }
1655
1656 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 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 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, }
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 pub fn auto_port(mut self, container_port: u16) -> Self {
1718 self.auto_ports.push(container_port);
1719 self
1720 }
1721
1722 pub fn no_auto_cleanup(mut self) -> Self {
1724 self.auto_cleanup = false;
1725 self
1726 }
1727
1728 fn find_available_port() -> Result<u16, Box<dyn std::error::Error + Send + Sync>> {
1730 use std::net::TcpListener;
1731
1732 let listener = TcpListener::bind("127.0.0.1:0")?;
1734 let addr = listener.local_addr()?;
1735 Ok(addr.port())
1736 }
1737
1738 pub fn start(&self) -> Result<ContainerInfo, Box<dyn std::error::Error + Send + Sync>> {
1740 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 let docker = Docker::connect_with_local_defaults()
1750 .map_err(|e| format!("Failed to connect to Docker: {}", e))?;
1751
1752 let mut port_bindings = PortMap::new();
1754 let mut auto_port_mappings = Vec::new();
1755
1756 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 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 auto_port_mappings.push((host_port, *container_port));
1778 }
1779
1780 let env_vars: Vec<String> = self.env.iter()
1782 .map(|(k, v)| format!("{}={}", k, v))
1783 .collect();
1784
1785 let cmd = if self.image.contains("alpine") || self.image.contains("busybox") || self.image.contains("ubuntu") {
1788 Some(vec!["sleep".to_string(), "3600".to_string()]) } 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 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 docker.start_container(&id, None::<bollard::query_parameters::StartContainerOptions>)
1812 .await
1813 .map_err(|e| format!("Failed to start container: {}", e))?;
1814
1815 self.wait_for_ready_async(&docker, &id).await?;
1817
1818 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 if container_info.auto_cleanup {
1844 register_container_for_cleanup(&container_info.container_id);
1845 }
1846
1847 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 pub fn stop(&self, container_id: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1863 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 let docker = Docker::connect_with_local_defaults()
1873 .map_err(|e| format!("Failed to connect to Docker: {}", e))?;
1874
1875 let stop_result = timeout(
1877 TokioDuration::from_secs(10), 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 }
1891 },
1892 Err(_) => {
1893 warn!("Container stop timeout for {}", container_id);
1894 },
1896 }
1897
1898 let remove_result = timeout(
1900 TokioDuration::from_secs(10), 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 }
1914 },
1915 Err(_) => {
1916 warn!("Container remove timeout for {}", container_id);
1917 },
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 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 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 info!("โ
Container {} is running and ready", container_id);
1961 return Ok(());
1962 }
1963 }
1964 }
1965 }
1966 }
1967
1968 sleep(TokioDuration::from_millis(500)).await;
1970 }
1971 }
1972}
1973
1974pub 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
1989pub 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
2002pub 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
2015pub 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
2028pub fn run_all() -> i32 {
2031 run_tests()
2032}
2033
2034