Skip to main content

cliffy_test/
result.rs

1//! Test result types with geometric information
2//!
3//! Test results are not just pass/fail - they carry geometric error information
4//! that helps diagnose and fix failures.
5
6use crate::error::GeometricError;
7use serde::{Deserialize, Serialize};
8
9/// Result of a geometric test
10///
11/// Unlike boolean tests, geometric tests provide rich error information
12/// when they fail, including distance from expected manifold and
13/// correction vectors.
14#[derive(Clone, Debug, Serialize, Deserialize)]
15pub enum TestResult {
16    /// Test passed - state lies on expected manifold
17    Pass,
18
19    /// Test failed with geometric error information
20    Fail(GeometricError),
21
22    /// Test was skipped (e.g., precondition not met)
23    Skipped(String),
24}
25
26impl TestResult {
27    /// Check if test passed
28    pub fn is_pass(&self) -> bool {
29        matches!(self, TestResult::Pass)
30    }
31
32    /// Check if test failed
33    pub fn is_fail(&self) -> bool {
34        matches!(self, TestResult::Fail(_))
35    }
36
37    /// Get the error if test failed
38    pub fn error(&self) -> Option<&GeometricError> {
39        match self {
40            TestResult::Fail(e) => Some(e),
41            _ => None,
42        }
43    }
44
45    /// Create a pass result
46    pub fn pass() -> Self {
47        TestResult::Pass
48    }
49
50    /// Create a fail result from geometric error
51    pub fn fail(error: GeometricError) -> Self {
52        TestResult::Fail(error)
53    }
54
55    /// Create a fail result from distance and description
56    pub fn fail_with_distance(distance: f64, description: impl Into<String>) -> Self {
57        TestResult::Fail(GeometricError::new(distance, description))
58    }
59
60    /// Create a skipped result
61    pub fn skipped(reason: impl Into<String>) -> Self {
62        TestResult::Skipped(reason.into())
63    }
64
65    /// Convert to standard Result type for use with ?
66    pub fn into_result(self) -> Result<(), GeometricError> {
67        match self {
68            TestResult::Pass => Ok(()),
69            TestResult::Fail(e) => Err(e),
70            TestResult::Skipped(reason) => Err(GeometricError::new(0.0, reason)),
71        }
72    }
73
74    /// Combine multiple test results
75    ///
76    /// Returns Pass only if all results are Pass.
77    /// Returns the first Fail if any failed.
78    pub fn combine(results: impl IntoIterator<Item = TestResult>) -> Self {
79        for result in results {
80            match result {
81                TestResult::Pass => continue,
82                TestResult::Fail(e) => return TestResult::Fail(e),
83                TestResult::Skipped(reason) => return TestResult::Skipped(reason),
84            }
85        }
86        TestResult::Pass
87    }
88}
89
90impl From<bool> for TestResult {
91    fn from(passed: bool) -> Self {
92        if passed {
93            TestResult::Pass
94        } else {
95            TestResult::Fail(GeometricError::new(1.0, "Boolean test failed"))
96        }
97    }
98}
99
100impl From<TestResult> for bool {
101    fn from(result: TestResult) -> Self {
102        result.is_pass()
103    }
104}
105
106/// Result of running an invariant test suite
107#[derive(Clone, Debug, Serialize, Deserialize)]
108pub struct InvariantTestReport {
109    /// Name of the invariant
110    pub name: String,
111
112    /// Category (impossible, rare, emergent)
113    pub category: InvariantCategory,
114
115    /// Number of samples tested
116    pub samples: usize,
117
118    /// Number of failures
119    pub failures: usize,
120
121    /// Observed failure rate (failures / samples)
122    pub failure_rate: f64,
123
124    /// Probability bound (for rare invariants)
125    pub probability_bound: Option<f64>,
126
127    /// Whether the invariant was verified
128    pub verified: bool,
129
130    /// Sample of failure errors (if any)
131    pub sample_errors: Vec<GeometricError>,
132
133    /// Confidence interval for the failure rate estimate (lower, upper)
134    ///
135    /// Computed using Hoeffding-based bounds from amari-flynn.
136    /// Only populated for Rare invariants verified with Monte Carlo.
137    pub confidence_interval: Option<(f64, f64)>,
138
139    /// Confidence level of the statistical verification (0.0 to 1.0)
140    ///
141    /// Computed from the Hoeffding bound given sample count and epsilon.
142    pub confidence_level: Option<f64>,
143}
144
145/// Category of invariant
146#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
147pub enum InvariantCategory {
148    /// Impossible (P = 0) - must never fail
149    Impossible,
150
151    /// Rare (0 < P << 1) - bounded failure probability
152    Rare,
153
154    /// Emergent (P > 0) - valid but unpredicted
155    Emergent,
156}
157
158impl InvariantTestReport {
159    /// Check if the invariant was violated
160    pub fn is_violated(&self) -> bool {
161        match self.category {
162            InvariantCategory::Impossible => self.failures > 0,
163            InvariantCategory::Rare => {
164                if let Some(bound) = self.probability_bound {
165                    self.failure_rate > bound
166                } else {
167                    self.failures > 0
168                }
169            }
170            InvariantCategory::Emergent => false, // Emergent behaviors are never violations
171        }
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_result_pass() {
181        let result = TestResult::pass();
182        assert!(result.is_pass());
183        assert!(!result.is_fail());
184    }
185
186    #[test]
187    fn test_result_fail() {
188        let result = TestResult::fail_with_distance(0.5, "Too far");
189        assert!(!result.is_pass());
190        assert!(result.is_fail());
191        assert_eq!(result.error().unwrap().distance, 0.5);
192    }
193
194    #[test]
195    fn test_result_combine() {
196        let results = vec![TestResult::Pass, TestResult::Pass, TestResult::Pass];
197        assert!(TestResult::combine(results).is_pass());
198
199        let results_with_fail = vec![
200            TestResult::Pass,
201            TestResult::fail_with_distance(0.1, "Error"),
202            TestResult::Pass,
203        ];
204        assert!(TestResult::combine(results_with_fail).is_fail());
205    }
206
207    #[test]
208    fn test_from_bool() {
209        let pass: TestResult = true.into();
210        assert!(pass.is_pass());
211
212        let fail: TestResult = false.into();
213        assert!(fail.is_fail());
214    }
215}