Skip to main content

shape_vm/feature_tests/
parity.rs

1//! Three-way parity testing infrastructure
2//!
3//! This module provides types and utilities for testing parity
4//! between the Interpreter, VM, and JIT backends.
5
6// ============================================================================
7// Three-Way Parity Testing
8// ============================================================================
9
10/// Result of running a test across all three backends
11#[derive(Debug, Clone)]
12pub struct ParityResult {
13    pub test_name: &'static str,
14    pub interpreter: ExecutionResult,
15    pub vm: ExecutionResult,
16    pub jit: ExecutionResult,
17}
18
19/// Result of executing a test on a single backend
20#[derive(Debug, Clone)]
21pub enum ExecutionResult {
22    /// Execution succeeded with this output
23    Success(String),
24    /// Execution failed with this error
25    Error(String),
26    /// Feature is not supported by this backend
27    NotSupported(&'static str),
28    /// Backend was skipped (disabled or unavailable)
29    Skipped(&'static str),
30}
31
32impl ExecutionResult {
33    pub fn is_success(&self) -> bool {
34        matches!(self, ExecutionResult::Success(_))
35    }
36
37    pub fn is_not_supported(&self) -> bool {
38        matches!(self, ExecutionResult::NotSupported(_))
39    }
40
41    pub fn is_skipped(&self) -> bool {
42        matches!(self, ExecutionResult::Skipped(_))
43    }
44
45    /// Get the output string if successful
46    pub fn output(&self) -> Option<&str> {
47        match self {
48            ExecutionResult::Success(s) => Some(s),
49            _ => None,
50        }
51    }
52}
53
54/// Status of parity check between backends
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub enum ParityStatus {
57    /// All backends that ran produced matching results
58    AllMatch,
59    /// Interpreter and VM produced different results
60    InterpreterVmMismatch { interpreter: String, vm: String },
61    /// Interpreter and JIT produced different results
62    InterpreterJitMismatch { interpreter: String, jit: String },
63    /// VM and JIT produced different results
64    VmJitMismatch { vm: String, jit: String },
65    /// Some backends were skipped or didn't support the feature
66    PartialSkipped { reason: String },
67    /// All backends failed
68    AllFailed,
69}
70
71impl ParityResult {
72    /// Check if all backends that succeeded have matching results
73    pub fn check_parity(&self) -> bool {
74        matches!(
75            self.parity_status(),
76            ParityStatus::AllMatch | ParityStatus::PartialSkipped { .. }
77        )
78    }
79
80    /// Get detailed parity status
81    pub fn parity_status(&self) -> ParityStatus {
82        let i_out = self.interpreter.output();
83        let v_out = self.vm.output();
84        let j_out = self.jit.output();
85
86        match (i_out, v_out, j_out) {
87            // All three succeeded - check all match
88            (Some(i), Some(v), Some(j)) => {
89                if i == v && v == j {
90                    ParityStatus::AllMatch
91                } else if i != v {
92                    ParityStatus::InterpreterVmMismatch {
93                        interpreter: i.to_string(),
94                        vm: v.to_string(),
95                    }
96                } else if i != j {
97                    ParityStatus::InterpreterJitMismatch {
98                        interpreter: i.to_string(),
99                        jit: j.to_string(),
100                    }
101                } else {
102                    ParityStatus::VmJitMismatch {
103                        vm: v.to_string(),
104                        jit: j.to_string(),
105                    }
106                }
107            }
108            // Two succeeded - check they match
109            (Some(i), Some(v), None) => {
110                if i == v {
111                    ParityStatus::PartialSkipped {
112                        reason: "JIT skipped/not supported".to_string(),
113                    }
114                } else {
115                    ParityStatus::InterpreterVmMismatch {
116                        interpreter: i.to_string(),
117                        vm: v.to_string(),
118                    }
119                }
120            }
121            (Some(i), None, Some(j)) => {
122                if i == j {
123                    ParityStatus::PartialSkipped {
124                        reason: "VM skipped/not supported".to_string(),
125                    }
126                } else {
127                    ParityStatus::InterpreterJitMismatch {
128                        interpreter: i.to_string(),
129                        jit: j.to_string(),
130                    }
131                }
132            }
133            (None, Some(v), Some(j)) => {
134                if v == j {
135                    ParityStatus::PartialSkipped {
136                        reason: "Interpreter skipped/not supported".to_string(),
137                    }
138                } else {
139                    ParityStatus::VmJitMismatch {
140                        vm: v.to_string(),
141                        jit: j.to_string(),
142                    }
143                }
144            }
145            // Only one succeeded
146            (Some(_), None, None) | (None, Some(_), None) | (None, None, Some(_)) => {
147                ParityStatus::PartialSkipped {
148                    reason: "Only one backend succeeded".to_string(),
149                }
150            }
151            // None succeeded
152            (None, None, None) => ParityStatus::AllFailed,
153        }
154    }
155
156    /// Check if this result is passing (parity maintained)
157    pub fn is_passing(&self) -> bool {
158        matches!(
159            self.parity_status(),
160            ParityStatus::AllMatch | ParityStatus::PartialSkipped { .. }
161        )
162    }
163
164    /// Format a diff for display when there's a mismatch
165    pub fn format_diff(&self) -> String {
166        match self.parity_status() {
167            ParityStatus::AllMatch => "All backends match".to_string(),
168            ParityStatus::InterpreterVmMismatch { interpreter, vm } => {
169                format!(
170                    "Interpreter vs VM mismatch:\n  Interpreter: {}\n  VM: {}",
171                    interpreter, vm
172                )
173            }
174            ParityStatus::InterpreterJitMismatch { interpreter, jit } => {
175                format!(
176                    "Interpreter vs JIT mismatch:\n  Interpreter: {}\n  JIT: {}",
177                    interpreter, jit
178                )
179            }
180            ParityStatus::VmJitMismatch { vm, jit } => {
181                format!("VM vs JIT mismatch:\n  VM: {}\n  JIT: {}", vm, jit)
182            }
183            ParityStatus::PartialSkipped { reason } => {
184                format!("Partial: {}", reason)
185            }
186            ParityStatus::AllFailed => "All backends failed".to_string(),
187        }
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_all_match() {
197        let result = ParityResult {
198            test_name: "test",
199            interpreter: ExecutionResult::Success("42".to_string()),
200            vm: ExecutionResult::Success("42".to_string()),
201            jit: ExecutionResult::Success("42".to_string()),
202        };
203        assert!(result.is_passing());
204        assert_eq!(result.parity_status(), ParityStatus::AllMatch);
205    }
206
207    #[test]
208    fn test_interpreter_vm_mismatch() {
209        let result = ParityResult {
210            test_name: "test",
211            interpreter: ExecutionResult::Success("42".to_string()),
212            vm: ExecutionResult::Success("43".to_string()),
213            jit: ExecutionResult::Success("42".to_string()),
214        };
215        assert!(!result.is_passing());
216        assert!(matches!(
217            result.parity_status(),
218            ParityStatus::InterpreterVmMismatch { .. }
219        ));
220    }
221
222    #[test]
223    fn test_partial_skipped() {
224        let result = ParityResult {
225            test_name: "test",
226            interpreter: ExecutionResult::Success("42".to_string()),
227            vm: ExecutionResult::Success("42".to_string()),
228            jit: ExecutionResult::NotSupported("pattern_def"),
229        };
230        assert!(result.is_passing());
231        assert!(matches!(
232            result.parity_status(),
233            ParityStatus::PartialSkipped { .. }
234        ));
235    }
236
237    #[test]
238    fn test_not_supported_variant() {
239        let result = ExecutionResult::NotSupported("async_block");
240        assert!(result.is_not_supported());
241        assert!(!result.is_success());
242    }
243}