Skip to main content

shape_vm/feature_tests/
parity_runner.rs

1//! Parity test runner for comparing backend execution results
2//!
3//! Runs feature tests across all backends and collects parity results.
4
5use super::FeatureTest;
6use super::backends::BackendExecutor;
7use super::parity::{ExecutionResult, ParityResult, ParityStatus};
8
9/// Runner that executes tests across multiple backends
10pub struct ParityRunner {
11    interpreter: Box<dyn BackendExecutor>,
12    vm: Box<dyn BackendExecutor>,
13    jit: Option<Box<dyn BackendExecutor>>,
14}
15
16impl ParityRunner {
17    /// Create a new parity runner with all backends
18    pub fn new(
19        interpreter: Box<dyn BackendExecutor>,
20        vm: Box<dyn BackendExecutor>,
21        jit: Option<Box<dyn BackendExecutor>>,
22    ) -> Self {
23        Self {
24            interpreter,
25            vm,
26            jit,
27        }
28    }
29
30    /// Create a runner with default backends
31    pub fn with_defaults() -> Self {
32        use super::backends::{InterpreterBackend, JITBackend, VMBackend};
33        Self {
34            interpreter: Box::new(InterpreterBackend),
35            vm: Box::new(VMBackend),
36            jit: Some(Box::new(JITBackend)),
37        }
38    }
39
40    /// Run a single test across all backends
41    pub fn run_test(&self, test: &FeatureTest) -> ParityResult {
42        let interpreter = if self.interpreter.is_available() {
43            self.interpreter.execute(test)
44        } else {
45            ExecutionResult::Skipped("Interpreter not available")
46        };
47
48        let vm = if self.vm.is_available() {
49            self.vm.execute(test)
50        } else {
51            ExecutionResult::Skipped("VM not available")
52        };
53
54        let jit = match &self.jit {
55            Some(jit_backend) if jit_backend.is_available() => jit_backend.execute(test),
56            Some(_) => ExecutionResult::Skipped("JIT not available"),
57            None => ExecutionResult::Skipped("JIT backend not configured"),
58        };
59
60        ParityResult {
61            test_name: test.name,
62            interpreter,
63            vm,
64            jit,
65        }
66    }
67
68    /// Run all tests and collect results
69    pub fn run_all(&self, tests: &[&FeatureTest]) -> ParityReport {
70        let mut results = Vec::with_capacity(tests.len());
71
72        for test in tests {
73            results.push(self.run_test(test));
74        }
75
76        ParityReport::from_results(results)
77    }
78}
79
80/// Report of all parity test results
81#[derive(Debug)]
82pub struct ParityReport {
83    pub results: Vec<ParityResult>,
84    pub total: usize,
85    pub passed: usize,
86    pub failed: usize,
87    pub partial: usize,
88}
89
90impl ParityReport {
91    /// Create a report from a list of results
92    pub fn from_results(results: Vec<ParityResult>) -> Self {
93        let total = results.len();
94        let mut passed = 0;
95        let mut failed = 0;
96        let mut partial = 0;
97
98        for result in &results {
99            match result.parity_status() {
100                ParityStatus::AllMatch => passed += 1,
101                ParityStatus::PartialSkipped { .. } => partial += 1,
102                ParityStatus::AllFailed => partial += 1, // Count as partial, not failed
103                _ => failed += 1,
104            }
105        }
106
107        Self {
108            results,
109            total,
110            passed,
111            failed,
112            partial,
113        }
114    }
115
116    /// Check if all tests passed (no mismatches)
117    pub fn all_passed(&self) -> bool {
118        self.failed == 0
119    }
120
121    /// Get all failing tests
122    pub fn failures(&self) -> Vec<&ParityResult> {
123        self.results.iter().filter(|r| !r.is_passing()).collect()
124    }
125
126    /// Format as text report
127    pub fn format_text(&self) -> String {
128        let mut output = String::new();
129
130        output.push_str("═══════════════════════════════════════════════════════════════\n");
131        output.push_str("                    PARITY TEST REPORT\n");
132        output.push_str("═══════════════════════════════════════════════════════════════\n\n");
133
134        output.push_str(&format!("Total tests: {}\n", self.total));
135        output.push_str(&format!("  ✓ Passed (all match): {}\n", self.passed));
136        output.push_str(&format!("  ~ Partial (some skipped): {}\n", self.partial));
137        output.push_str(&format!("  ✗ Failed (mismatch): {}\n", self.failed));
138        output.push('\n');
139
140        if self.failed > 0 {
141            output.push_str("───────────────────────────────────────────────────────────────\n");
142            output.push_str("                      FAILURES\n");
143            output.push_str("───────────────────────────────────────────────────────────────\n\n");
144
145            for result in self.failures() {
146                output.push_str(&format!("Test: {}\n", result.test_name));
147                output.push_str(&format!("  {}\n\n", result.format_diff()));
148            }
149        }
150
151        output.push_str("═══════════════════════════════════════════════════════════════\n");
152
153        if self.all_passed() {
154            output.push_str("                    ALL TESTS PASSED\n");
155        } else {
156            output.push_str(&format!("                    {} FAILURES\n", self.failed));
157        }
158
159        output.push_str("═══════════════════════════════════════════════════════════════\n");
160
161        output
162    }
163
164    /// Format as JSON
165    pub fn format_json(&self) -> String {
166        let json = serde_json::json!({
167            "total": self.total,
168            "passed": self.passed,
169            "partial": self.partial,
170            "failed": self.failed,
171            "results": self.results.iter().map(|r| {
172                serde_json::json!({
173                    "name": r.test_name,
174                    "passing": r.is_passing(),
175                    "status": format!("{:?}", r.parity_status()),
176                })
177            }).collect::<Vec<_>>(),
178        });
179        serde_json::to_string_pretty(&json).unwrap_or_default()
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn test_report_from_results() {
189        let results = vec![
190            ParityResult {
191                test_name: "test1",
192                interpreter: ExecutionResult::Success("1".to_string()),
193                vm: ExecutionResult::Success("1".to_string()),
194                jit: ExecutionResult::Success("1".to_string()),
195            },
196            ParityResult {
197                test_name: "test2",
198                interpreter: ExecutionResult::Success("2".to_string()),
199                vm: ExecutionResult::Success("3".to_string()), // Mismatch!
200                jit: ExecutionResult::Success("2".to_string()),
201            },
202        ];
203
204        let report = ParityReport::from_results(results);
205        assert_eq!(report.total, 2);
206        assert_eq!(report.passed, 1);
207        assert_eq!(report.failed, 1);
208        assert!(!report.all_passed());
209    }
210
211    #[test]
212    fn test_report_format_text() {
213        let results = vec![ParityResult {
214            test_name: "simple_add",
215            interpreter: ExecutionResult::Success("42".to_string()),
216            vm: ExecutionResult::Success("42".to_string()),
217            jit: ExecutionResult::Success("42".to_string()),
218        }];
219
220        let report = ParityReport::from_results(results);
221        let text = report.format_text();
222        assert!(text.contains("ALL TESTS PASSED"));
223        assert!(text.contains("Total tests: 1"));
224    }
225}