Skip to main content

xlog_cuda_tests/harness/
diagnostics.rs

1//! Test diagnostics, failure reporting, and result tracking.
2
3use std::time::{Duration, Instant};
4
5/// Rich failure diagnostic with context.
6#[derive(Debug, Clone)]
7pub struct FailureDiagnostic {
8    pub category: &'static str,
9    pub test_name: &'static str,
10    pub input_size: usize,
11    pub input_sample: String,
12    pub expected_sample: String,
13    pub actual_sample: String,
14    pub first_diff_index: Option<usize>,
15    pub diff_count: usize,
16    pub error_message: String,
17}
18
19impl FailureDiagnostic {
20    /// Create a new failure diagnostic.
21    pub fn new(
22        category: &'static str,
23        test_name: &'static str,
24        input_size: usize,
25        error_message: String,
26    ) -> Self {
27        Self {
28            category,
29            test_name,
30            input_size,
31            input_sample: String::new(),
32            expected_sample: String::new(),
33            actual_sample: String::new(),
34            first_diff_index: None,
35            diff_count: 0,
36            error_message,
37        }
38    }
39
40    /// Add input sample (first N elements).
41    pub fn with_input_sample(mut self, sample: String) -> Self {
42        self.input_sample = sample;
43        self
44    }
45
46    /// Add expected/actual samples.
47    pub fn with_comparison(
48        mut self,
49        expected: String,
50        actual: String,
51        first_diff: Option<usize>,
52        diff_count: usize,
53    ) -> Self {
54        self.expected_sample = expected;
55        self.actual_sample = actual;
56        self.first_diff_index = first_diff;
57        self.diff_count = diff_count;
58        self
59    }
60
61    /// Generate human-readable failure report.
62    pub fn report(&self) -> String {
63        let mut report = String::new();
64        report.push_str(&format!(
65            "=== FAILURE: {}/{} ===\n",
66            self.category, self.test_name
67        ));
68        report.push_str(&format!("Input size: {}\n", self.input_size));
69        report.push_str(&format!("Error: {}\n", self.error_message));
70
71        if !self.input_sample.is_empty() {
72            report.push_str(&format!("Input sample: {}\n", self.input_sample));
73        }
74
75        if self.diff_count > 0 {
76            report.push_str(&format!("Differences: {} total\n", self.diff_count));
77            if let Some(idx) = self.first_diff_index {
78                report.push_str(&format!("First diff at index: {}\n", idx));
79            }
80            report.push_str(&format!("Expected: {}\n", self.expected_sample));
81            report.push_str(&format!("Actual:   {}\n", self.actual_sample));
82        }
83
84        report.push_str("===\n");
85        report
86    }
87}
88
89/// Test execution status.
90#[derive(Debug, Clone)]
91pub enum TestStatus {
92    Passed,
93    Failed,
94    Skipped { reason: &'static str },
95    Error { message: String },
96}
97
98impl TestStatus {
99    pub fn is_passed(&self) -> bool {
100        matches!(self, TestStatus::Passed)
101    }
102
103    pub fn is_failed(&self) -> bool {
104        matches!(self, TestStatus::Failed | TestStatus::Error { .. })
105    }
106}
107
108/// Individual test result.
109#[derive(Debug, Clone)]
110pub struct TestResult {
111    pub name: &'static str,
112    pub status: TestStatus,
113    pub duration: Duration,
114    pub diagnostic: Option<FailureDiagnostic>,
115}
116
117impl TestResult {
118    pub fn passed(name: &'static str, duration: Duration) -> Self {
119        Self {
120            name,
121            status: TestStatus::Passed,
122            duration,
123            diagnostic: None,
124        }
125    }
126
127    pub fn failed(name: &'static str, duration: Duration, diagnostic: FailureDiagnostic) -> Self {
128        Self {
129            name,
130            status: TestStatus::Failed,
131            duration,
132            diagnostic: Some(diagnostic),
133        }
134    }
135
136    pub fn skipped(name: &'static str, reason: &'static str) -> Self {
137        Self {
138            name,
139            status: TestStatus::Skipped { reason },
140            duration: Duration::ZERO,
141            diagnostic: None,
142        }
143    }
144
145    pub fn error(name: &'static str, duration: Duration, message: String) -> Self {
146        Self {
147            name,
148            status: TestStatus::Error { message },
149            duration,
150            diagnostic: None,
151        }
152    }
153}
154
155/// Category-level result aggregation.
156#[derive(Debug, Clone)]
157pub struct CategoryResult {
158    pub name: &'static str,
159    pub tests: Vec<TestResult>,
160    pub duration: Duration,
161}
162
163impl CategoryResult {
164    pub fn new(name: &'static str) -> Self {
165        Self {
166            name,
167            tests: Vec::new(),
168            duration: Duration::ZERO,
169        }
170    }
171
172    pub fn add_result(&mut self, result: TestResult) {
173        self.tests.push(result);
174    }
175
176    pub fn set_duration(&mut self, duration: Duration) {
177        self.duration = duration;
178    }
179
180    pub fn passed_count(&self) -> usize {
181        self.tests.iter().filter(|t| t.status.is_passed()).count()
182    }
183
184    pub fn failed_count(&self) -> usize {
185        self.tests.iter().filter(|t| t.status.is_failed()).count()
186    }
187
188    pub fn skipped_count(&self) -> usize {
189        self.tests
190            .iter()
191            .filter(|t| matches!(t.status, TestStatus::Skipped { .. }))
192            .count()
193    }
194
195    pub fn total_count(&self) -> usize {
196        self.tests.len()
197    }
198
199    pub fn all_passed(&self) -> bool {
200        self.tests
201            .iter()
202            .all(|t| t.status.is_passed() || matches!(t.status, TestStatus::Skipped { .. }))
203    }
204}
205
206/// Full certification results.
207#[derive(Debug)]
208pub struct CertificationResults {
209    pub categories: Vec<CategoryResult>,
210    pub start_time: Instant,
211    pub total_duration: Duration,
212}
213
214impl CertificationResults {
215    pub fn new() -> Self {
216        Self {
217            categories: Vec::new(),
218            start_time: Instant::now(),
219            total_duration: Duration::ZERO,
220        }
221    }
222
223    /// Run a category and record results.
224    pub fn run_category<F>(&mut self, _name: &'static str, f: F)
225    where
226        F: FnOnce() -> CategoryResult,
227    {
228        let start = Instant::now();
229        let mut result = f();
230        result.set_duration(start.elapsed());
231        self.categories.push(result);
232    }
233
234    /// Add a pre-computed category result.
235    pub fn add_category(&mut self, result: CategoryResult) {
236        self.categories.push(result);
237    }
238
239    /// Finalize results and compute total duration.
240    pub fn finalize(&mut self) {
241        self.total_duration = self.start_time.elapsed();
242    }
243
244    pub fn total_tests(&self) -> usize {
245        self.categories.iter().map(|c| c.total_count()).sum()
246    }
247
248    pub fn total_passed(&self) -> usize {
249        self.categories.iter().map(|c| c.passed_count()).sum()
250    }
251
252    pub fn total_failed(&self) -> usize {
253        self.categories.iter().map(|c| c.failed_count()).sum()
254    }
255
256    pub fn total_skipped(&self) -> usize {
257        self.categories.iter().map(|c| c.skipped_count()).sum()
258    }
259
260    pub fn all_passed(&self) -> bool {
261        self.categories.iter().all(|c| c.all_passed())
262    }
263
264    /// Print summary to stdout.
265    pub fn print_summary(&self) {
266        println!("\n========== CERTIFICATION RESULTS ==========");
267        println!("Total Duration: {:.2}s", self.total_duration.as_secs_f64());
268        println!();
269
270        for cat in &self.categories {
271            let status = if cat.all_passed() { "PASS" } else { "FAIL" };
272            println!(
273                "[{}] {}: {}/{} passed, {} skipped ({:.2}s)",
274                status,
275                cat.name,
276                cat.passed_count(),
277                cat.total_count(),
278                cat.skipped_count(),
279                cat.duration.as_secs_f64()
280            );
281        }
282
283        println!();
284        println!("========== SUMMARY ==========");
285        println!("Total Tests:   {}", self.total_tests());
286        println!("Passed:        {}", self.total_passed());
287        println!("Failed:        {}", self.total_failed());
288        println!("Skipped:       {}", self.total_skipped());
289        println!(
290            "Pass Rate:     {:.1}%",
291            (self.total_passed() as f64 / self.total_tests().max(1) as f64) * 100.0
292        );
293        println!("=====================================\n");
294    }
295
296    /// Generate detailed failure report.
297    pub fn failure_report(&self) -> String {
298        let mut report = String::new();
299
300        for cat in &self.categories {
301            for test in &cat.tests {
302                if let Some(diag) = &test.diagnostic {
303                    report.push_str(&diag.report());
304                    report.push('\n');
305                } else if let TestStatus::Error { message } = &test.status {
306                    report.push_str(&format!("=== ERROR: {}/{} ===\n", cat.name, test.name));
307                    report.push_str(&format!("Error: {}\n", message));
308                    report.push_str("===\n\n");
309                }
310            }
311        }
312
313        report
314    }
315}
316
317impl Default for CertificationResults {
318    fn default() -> Self {
319        Self::new()
320    }
321}
322
323/// Helper macro for running a test and capturing results.
324#[macro_export]
325macro_rules! run_test {
326    ($results:expr, $name:expr, $body:expr) => {{
327        let start = std::time::Instant::now();
328        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body));
329        let duration = start.elapsed();
330
331        match result {
332            Ok(()) => $crate::harness::TestResult::passed($name, duration),
333            Err(e) => {
334                let message = if let Some(s) = e.downcast_ref::<&str>() {
335                    s.to_string()
336                } else if let Some(s) = e.downcast_ref::<String>() {
337                    s.clone()
338                } else {
339                    "Unknown panic".to_string()
340                };
341                $crate::harness::TestResult::error($name, duration, message)
342            }
343        }
344    }};
345}