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
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
25thread_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
36pub fn clear_test_registry() {
40 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
48pub 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>, 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, tags: self.tags.clone(),
68 timeout: self.timeout.clone(),
69 status: self.status.clone(),
70 }
71 }
72}
73
74#[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 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 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 pub fn has_data(&self, key: &str) -> bool {
114 self.data.contains_key(key)
115 }
116
117 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 }
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(), }
138 }
139}
140
141#[derive(Debug, Clone)]
142pub struct DockerHandle {
143 pub container_id: String,
144 pub ports: Vec<(u16, u16)>, }
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
184pub 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
258pub 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 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 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 for hook in before_all_hooks {
292 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 }
304 Ok(Err(e)) => {
305 error!("ā before_all hook failed: {}", e);
306 return 1; }
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; }
319 }
320 }
321
322 info!("ā
before_all hooks completed");
323
324 let global_ctx = get_global_context();
326 clear_global_context(); 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 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 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 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 if !config.skip_hooks.unwrap_or(false) && !after_all_hooks.is_empty() {
370 info!("š Running {} after_all hooks", after_all_hooks.len());
371
372 for hook in after_all_hooks {
374 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 }
386 Ok(Err(e)) => {
387 warn!("ā ļø after_all hook failed: {}", e);
388 }
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 }
401 }
402 }
403
404 info!("ā
after_all hooks completed");
405 }
406
407 let total_time = start_time.elapsed();
408
409 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 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
449fn filter_and_sort_test_indices(tests: &[TestCase], config: &TestConfig) -> Vec<usize> {
452 let mut indices: Vec<usize> = (0..tests.len()).collect();
453
454 if let Some(ref filter) = config.filter {
456 indices.retain(|&idx| tests[idx].name.contains(filter));
457 }
458
459 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 if let Some(seed) = config.shuffle_seed {
469 use std::collections::hash_map::DefaultHasher;
470 use std::hash::{Hash, Hasher};
471
472 let mut hasher = DefaultHasher::new();
474 seed.hash(&mut hasher);
475 let mut rng_state = hasher.finish();
476
477 for i in (1..indices.len()).rev() {
479 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::prelude::*;
507
508
509
510 let pool = rayon::ThreadPoolBuilder::new()
512 .num_threads(max_workers)
513 .build()
514 .expect("Failed to create thread pool");
515
516
517
518 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 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 let results: Vec<_> = pool.install(|| {
538 test_indices.par_iter().enumerate().map(|(i, &idx)| {
539 let (name, tags, timeout, status) = &test_data[i];
541 let mut test = TestCase {
542 name: name.clone(),
543 test_fn: None, tags: tags.clone(),
545 timeout: *timeout,
546 status: status.clone(),
547 };
548
549 let test_fn = test_functions[i].clone();
550
551 let before_hooks = before_each_hooks.clone();
553 let after_hooks = after_each_hooks.clone();
554
555 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 for (idx, test_result) in results {
570 tests[idx] = test_result;
571
572 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 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 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 let mut ctx = TestContext::new();
646
647 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 if !config.skip_hooks.unwrap_or(false) {
658 for hook in before_each_hooks.iter_mut() {
659 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 }
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 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 if !config.skip_hooks.unwrap_or(false) {
705 for hook in after_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 warn!("ā ļø after_each hook failed: {}", e);
720 }
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 }
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 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 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 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 let mut ctx = TestContext::new();
791 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 if !config.skip_hooks.unwrap_or(false) {
802 for hook in before_each_hooks.iter() {
803 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 }
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 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 if !config.skip_hooks.unwrap_or(false) {
855 for hook in after_each_hooks.iter() {
856 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 }
868 Ok(Err(e)) => {
869 warn!("ā ļø after_each hook failed: {}", e);
870 }
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 }
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 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 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 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 let recv_result = match config.strategy {
952 TimeoutStrategy::Simple => {
953 rx.recv_timeout(timeout)
955 }
956 TimeoutStrategy::Aggressive => {
957 rx.recv_timeout(timeout)
959 }
960 TimeoutStrategy::Graceful(cleanup_time) => {
961 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 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 match test_result {
981 Ok(()) => {
982 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 Err(e)
993 }
994 }
995 }
996 Ok((Err(panic_info), _)) => {
997 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 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); 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 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 }
1039
1040#[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,
1075 Aggressive,
1077 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
1100fn 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 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 let final_path = if std::path::Path::new(output_path).is_absolute() {
1114 output_path.to_string()
1115 } else {
1116 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.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 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 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 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 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.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 std::fs::write(&final_path, html)?;
1404
1405 info!("š HTML report written to: {}", final_path);
1407
1408 Ok(())
1409}
1410
1411#[macro_export]
1417macro_rules! test_function {
1418 ($name:ident, $test_fn:expr) => {
1419 #[test]
1420 fn $name() {
1421 let _ = env_logger::try_init();
1423
1424 let result = ($test_fn)(&mut rust_test_harness::TestContext::new());
1426
1427 match result {
1429 Ok(_) => {
1430 }
1432 Err(e) => {
1433 panic!("ā Test '{}' failed: {:?}", stringify!($name), e);
1434 }
1435 }
1436 }
1437 };
1438}
1439
1440#[macro_export]
1443macro_rules! test_named {
1444 ($name:expr, $test_fn:expr) => {
1445 #[test]
1446 fn test_named_function() {
1447 let _ = env_logger::try_init();
1449
1450 let result = ($test_fn)(&mut rust_test_harness::TestContext::new());
1452
1453 match result {
1455 Ok(_) => {
1456 }
1458 Err(e) => {
1459 panic!("ā Test '{}' failed: {:?}", $name, e);
1460 }
1461 }
1462 }
1463 };
1464}
1465
1466#[macro_export]
1469macro_rules! test_async {
1470 ($name:ident, $test_fn:expr) => {
1471 #[tokio::test]
1472 async fn $name() {
1473 let _ = env_logger::try_init();
1475
1476 let result = ($test_fn)(&mut rust_test_harness::TestContext::new()).await;
1478
1479 match result {
1481 Ok(_) => {
1482 }
1484 Err(e) => {
1485 panic!("ā Async test '{}' failed: {:?}", stringify!($name), e);
1486 }
1487 }
1488 }
1489 };
1490}
1491
1492#[macro_export]
1517macro_rules! test_case {
1518 ($name:ident, $test_fn:expr) => {
1519 #[test]
1520 #[allow(unused_imports)]
1521 fn $name() {
1522 let _ = env_logger::try_init();
1524
1525 let result: rust_test_harness::TestResult = ($test_fn)(&mut rust_test_harness::TestContext::new());
1527
1528 match result {
1530 Ok(_) => {
1531 }
1533 Err(e) => {
1534 panic!("Test failed: {:?}", e);
1535 }
1536 }
1537 }
1538 };
1539}
1540
1541#[macro_export]
1557macro_rules! test_case_named {
1558 ($name:ident, $test_fn:expr) => {
1559 #[test]
1560 fn $name() {
1561 let _ = env_logger::try_init();
1563
1564 let result: rust_test_harness::TestResult = ($test_fn)(&mut rust_test_harness::TestContext::new());
1566
1567 match result {
1569 Ok(_) => {
1570 }
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)>, 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 pub fn start(&self) -> Result<String, Box<dyn std::error::Error + Send + Sync>> {
1624 #[cfg(feature = "docker")]
1625 {
1626 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 let docker = Docker::connect_with_local_defaults()
1636 .map_err(|e| format!("Failed to connect to Docker: {}", e))?;
1637
1638 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 let env_vars: Vec<String> = self.env.iter()
1650 .map(|(k, v)| format!("{}={}", k, v))
1651 .collect();
1652
1653 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 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 docker.start_container(&id, None::<bollard::query_parameters::StartContainerOptions>)
1672 .await
1673 .map_err(|e| format!("Failed to start container: {}", e))?;
1674
1675 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 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 std::thread::sleep(Duration::from_millis(100));
1698
1699 info!("ā
Mock container {} started successfully", container_id);
1700 Ok(container_id)
1701 }
1702 }
1703
1704 pub fn stop(&self, container_id: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
1706 #[cfg(feature = "docker")]
1707 {
1708 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 docker.stop_container(container_id, None::<bollard::query_parameters::StopContainerOptions>)
1720 .await
1721 .map_err(|e| format!("Failed to stop container: {}", e))?;
1722
1723 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 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 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 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 info!("ā
Container {} is running and ready", container_id);
1779 return Ok(());
1780 }
1781 }
1782 }
1783 }
1784 }
1785
1786 sleep(TokioDuration::from_millis(500)).await;
1788 }
1789 }
1790}
1791
1792pub 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
1807pub 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
1820pub 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
1833pub 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
1846pub fn run_all() -> i32 {
1849 run_tests()
1850}
1851
1852