1use crate::{ContextManager, TestDescriptor};
4use std::collections::HashMap;
5use tracing::{debug, error, info};
6
7#[derive(Debug)]
9pub struct TestResults {
10 pub total: usize,
11 pub passed: usize,
12 pub failed: usize,
13 pub failures: Vec<TestFailure>,
14}
15
16#[derive(Debug, Clone)]
18pub struct TestFailure {
19 pub name: String,
20 pub file: String,
21 pub line: u32,
22 pub error: String,
23}
24
25impl TestFailure {
26 fn context_start_failed(test: &TestDescriptor, error: String) -> Self {
28 Self {
29 name: test.name.to_string(),
30 file: test.file.to_string(),
31 line: test.line,
32 error: format!("Context startup failed: {}", error),
33 }
34 }
35
36 fn hook_failed(test: &TestDescriptor, hook_name: &str, error: String) -> Self {
38 Self {
39 name: test.name.to_string(),
40 file: test.file.to_string(),
41 line: test.line,
42 error: format!("Hook '{}' failed: {}", hook_name, error),
43 }
44 }
45
46 fn test_failed(test: &TestDescriptor, error: String) -> Self {
48 Self {
49 name: test.name.to_string(),
50 file: test.file.to_string(),
51 line: test.line,
52 error,
53 }
54 }
55}
56
57impl TestResults {
58 pub fn all_passed(&self) -> bool {
60 self.failed == 0
61 }
62
63 pub fn print_summary(&self) {
65 println!("\n{}", "=".repeat(80));
66 println!("📊 Test Results Summary");
67 println!("{}", "=".repeat(80));
68
69 if self.failed == 0 {
70 println!(" ✅ All tests passed!");
71 } else {
72 println!(" ⚠️ Some tests failed");
73 }
74
75 println!();
76 println!(" Total: {}", self.total);
77 println!(" ✅ Passed: {}", self.passed);
78
79 if !self.failures.is_empty() {
80 println!(" ❌ Failed: {}", self.failed);
81 println!("\n{}", "-".repeat(80));
82 println!("❌ Failed Tests:");
83 println!("{}", "-".repeat(80));
84 for failure in &self.failures {
85 println!("\n Test: {}", failure.name);
86 println!(" Location: {}:{}", failure.file, failure.line);
87 println!(" Error: {}", failure.error);
88 }
89 }
90
91 println!("{}", "=".repeat(80));
92 }
93}
94
95pub fn collect_tests() -> Vec<&'static TestDescriptor> {
97 inventory::iter::<TestDescriptor>().collect()
98}
99
100#[tracing::instrument(name = "test_run", skip_all, fields(total_tests, context_types))]
105pub async fn run_all_tests() -> TestResults {
106 let _ = tracing_subscriber::fmt()
108 .with_target(false)
109 .with_test_writer()
110 .try_init();
111
112 let tests = collect_tests();
113
114 tracing::Span::current().record("total_tests", tests.len());
115 info!("Starting integration test run");
116
117 let mut grouped_tests: HashMap<&str, Vec<&TestDescriptor>> = HashMap::new();
119 for test in tests.iter() {
120 grouped_tests
121 .entry(test.context_type)
122 .or_default()
123 .push(test);
124 }
125
126 tracing::Span::current().record("context_types", grouped_tests.len());
127 info!("Grouped tests by context type");
128
129 run_all_tests_impl(tests, grouped_tests).await
130}
131
132pub async fn run_all_tests_impl(
137 tests: Vec<&'static TestDescriptor>,
138 grouped_tests: HashMap<&'static str, Vec<&'static TestDescriptor>>,
139) -> TestResults {
140 info!("Starting integration test run");
141
142 let futures: Vec<_> = grouped_tests
144 .into_values()
145 .map(|group| {
146 let group_slice: &'static [&'static TestDescriptor] = Box::leak(group.into_boxed_slice());
147 async move {
148 (group_slice[0].run_group)(group_slice).await
151 }
152 })
153 .collect();
154
155 let group_results = futures::future::join_all(futures).await;
156
157 let mut passed = 0;
159 let mut failed = 0;
160 let mut failures = Vec::new();
161
162 for group_result in group_results {
163 passed += group_result.passed;
164 failed += group_result.failed;
165 failures.extend(group_result.failures);
166 }
167
168 let results = TestResults {
169 total: tests.len(),
170 passed,
171 failed,
172 failures,
173 };
174
175 info!(
176 total = results.total,
177 passed = results.passed,
178 failed = results.failed,
179 "Test run completed"
180 );
181
182 results.print_summary();
183
184 results
185}
186
187pub struct ContextGroupResult {
191 pub passed: usize,
192 pub failed: usize,
193 pub failures: Vec<TestFailure>,
194}
195
196#[tracing::instrument(
201 name = "context_group",
202 skip_all,
203 fields(
204 context_type = %context_type,
205 test_count = tests.len(),
206 status = tracing::field::Empty,
207 )
208)]
209pub async fn run_context_group<C: Send + 'static>(
210 context_type: &str,
211 tests: &[(crate::TestFn<C>, &'static TestDescriptor)],
212 manager: &dyn ContextManager<C>,
213 hooks: crate::Hooks<C>,
214) -> ContextGroupResult {
215 info!(
216 context_type = context_type,
217 test_count = tests.len(),
218 "CONTEXT_GROUP_START"
219 );
220
221 let mut passed = 0;
222 let mut failed = 0;
223 let mut failures = Vec::new();
224
225 tracing::Span::current().record("status", "starting context");
227 let ctx = match manager.start().await {
228 Ok(ctx) => ctx,
229 Err(e) => {
230 tracing::Span::current().record("status", "❌ failed to start");
231 let error_msg = e.to_string();
232 error!(error = %e, "Failed to start context");
233
234 for &(_, descriptor) in tests {
236 failures.push(TestFailure::context_start_failed(descriptor, error_msg.clone()));
237 }
238
239 return ContextGroupResult {
240 passed: 0,
241 failed: tests.len(),
242 failures,
243 };
244 }
245 };
246
247 tracing::Span::current().record("status", "✅ context ready");
248
249 if let Some(before_all_fn) = hooks.before_all
251 && let Err(e) = before_all_fn(&ctx).await {
252 error!(
254 "before_all hook failed, skipping all tests in context {}",
255 context_type
256 );
257 tracing::Span::current().record("status", "❌ before_all failed");
258
259 for &(_, descriptor) in tests {
260 failures.push(TestFailure::hook_failed(
261 descriptor,
262 "before_all",
263 e.to_string(),
264 ));
265 }
266
267 if let Err(stop_err) = manager.stop(ctx).await {
269 error!(error = %stop_err, "Failed to stop context after before_all failure");
270 }
271
272 return ContextGroupResult {
273 passed: 0,
274 failed: tests.len(),
275 failures,
276 };
277 }
278
279 for &(test_fn, descriptor) in tests {
281 if let Some(before_each_fn) = hooks.before_each
283 && let Err(e) = before_each_fn(&ctx).await {
284 failed += 1;
285 failures.push(TestFailure::hook_failed(
286 descriptor,
287 "before_each",
288 e.to_string(),
289 ));
290
291 if let Some(after_each_fn) = hooks.after_each {
293 let _ = after_each_fn(&ctx).await;
294 }
295
296 continue; }
298
299 let test_result = run_test(descriptor, test_fn, &ctx).await;
301
302 if let Some(after_each_fn) = hooks.after_each
304 && let Err(e) = after_each_fn(&ctx).await {
305 failed += 1;
307 failures.push(TestFailure::hook_failed(
308 descriptor,
309 "after_each",
310 e.to_string(),
311 ));
312 continue;
313 }
314
315 match test_result {
317 Ok(()) => {
318 passed += 1;
319 }
320 Err(e) => {
321 failed += 1;
322 failures.push(TestFailure::test_failed(descriptor, e.to_string()));
323 }
324 }
325 }
326
327 if let Some(after_all_fn) = hooks.after_all
329 && let Err(e) = after_all_fn(&ctx).await {
330 tracing::warn!("after_all hook failed (non-fatal): {}", e);
331 }
332
333 tracing::Span::current().record("status", "🛑 stopping");
335 if let Err(e) = manager.stop(ctx).await {
336 error!(error = %e, "Failed to stop context");
337 tracing::Span::current().record("status", "⚠️ completed with stop error");
338 } else {
339 tracing::Span::current().record("status", "✅ completed");
340 }
341
342 info!("CONTEXT_GROUP_END");
343
344 ContextGroupResult {
345 passed,
346 failed,
347 failures,
348 }
349}
350
351#[tracing::instrument(
353 name = "test",
354 skip_all,
355 fields(
356 name = %descriptor.name,
357 context_type = %descriptor.context_type,
358 file = %descriptor.file,
359 line = descriptor.line,
360 result = tracing::field::Empty,
361 )
362)]
363async fn run_test<C>(
364 descriptor: &TestDescriptor,
365 test_fn: crate::TestFn<C>,
366 ctx: &C,
367) -> Result<(), Box<dyn std::error::Error + Send>> {
368 debug!("Running test");
369
370 match test_fn(ctx).await {
371 Ok(()) => {
372 tracing::Span::current().record("result", "✅ passed");
373 info!(name = descriptor.name, result = "✅ passed", "TEST_PASSED");
374 Ok(())
375 }
376 Err(e) => {
377 tracing::Span::current().record("result", "❌ failed");
378 error!(name = descriptor.name, error = %e, "TEST_FAILED");
379 Err(e)
380 }
381 }
382}
383
384#[macro_export]
398macro_rules! test_runner {
399 () => {
400 #[tokio::test]
401 async fn __run_all_admixture_tests() {
402 let results = $crate::runner::run_all_tests().await;
403 assert!(results.all_passed(), "{} test(s) failed", results.failed);
404 }
405 };
406}