Skip to main content

moloch_verify/
properties.rs

1//! Property-based testing for chain operations.
2
3use std::fmt::Debug;
4
5/// Result of a property test.
6#[derive(Debug, Clone)]
7pub enum PropertyResult {
8    /// Property holds.
9    Passed,
10    /// Property failed with counterexample.
11    Failed {
12        /// Counterexample description.
13        counterexample: String,
14        /// Number of tests before failure.
15        tests_run: usize,
16    },
17    /// Property testing was skipped.
18    Skipped {
19        /// Reason for skipping.
20        reason: String,
21    },
22}
23
24impl PropertyResult {
25    /// Check if the property passed.
26    pub fn is_passed(&self) -> bool {
27        matches!(self, Self::Passed)
28    }
29
30    /// Check if the property failed.
31    pub fn is_failed(&self) -> bool {
32        matches!(self, Self::Failed { .. })
33    }
34}
35
36/// A property that should hold for all inputs.
37pub trait Property<I> {
38    /// Name of the property.
39    fn name(&self) -> &str;
40
41    /// Check if the property holds for an input.
42    fn check(&self, input: &I) -> bool;
43
44    /// Describe a failing input for debugging.
45    fn describe_failure(&self, input: &I) -> String;
46}
47
48/// Property test runner.
49pub struct PropertyTest<I, G> {
50    /// Property to test.
51    property: Box<dyn Property<I>>,
52    /// Input generator.
53    generator: G,
54    /// Maximum iterations.
55    max_iterations: usize,
56    /// Seed for reproducibility.
57    seed: u64,
58}
59
60impl<I, G> PropertyTest<I, G>
61where
62    G: Iterator<Item = I>,
63    I: Debug,
64{
65    /// Create a new property test.
66    pub fn new(property: impl Property<I> + 'static, generator: G) -> Self {
67        Self {
68            property: Box::new(property),
69            generator,
70            max_iterations: 100,
71            seed: 0,
72        }
73    }
74
75    /// Set maximum iterations.
76    pub fn with_iterations(mut self, n: usize) -> Self {
77        self.max_iterations = n;
78        self
79    }
80
81    /// Set seed for reproducibility.
82    pub fn with_seed(mut self, seed: u64) -> Self {
83        self.seed = seed;
84        self
85    }
86
87    /// Run the property test.
88    pub fn run(self) -> PropertyResult {
89        for (i, input) in self.generator.take(self.max_iterations).enumerate() {
90            if !self.property.check(&input) {
91                return PropertyResult::Failed {
92                    counterexample: self.property.describe_failure(&input),
93                    tests_run: i + 1,
94                };
95            }
96        }
97        PropertyResult::Passed
98    }
99}
100
101/// Built-in property: append-only semantics.
102pub struct AppendOnlyProperty;
103
104impl Property<(Vec<u8>, Vec<u8>)> for AppendOnlyProperty {
105    fn name(&self) -> &str {
106        "append_only"
107    }
108
109    fn check(&self, (before, after): &(Vec<u8>, Vec<u8>)) -> bool {
110        // After must start with before (append-only)
111        after.starts_with(before)
112    }
113
114    fn describe_failure(&self, (before, after): &(Vec<u8>, Vec<u8>)) -> String {
115        format!(
116            "append-only violated: before={} bytes, after={} bytes, prefix mismatch",
117            before.len(),
118            after.len()
119        )
120    }
121}
122
123/// Built-in property: idempotent operations.
124pub struct IdempotentProperty<F> {
125    /// Operation to test.
126    operation: F,
127}
128
129impl<F, I, O> Property<I> for IdempotentProperty<F>
130where
131    F: Fn(&I) -> O,
132    O: PartialEq + Debug,
133    I: Clone + Debug,
134{
135    fn name(&self) -> &str {
136        "idempotent"
137    }
138
139    fn check(&self, input: &I) -> bool {
140        let first = (self.operation)(input);
141        let second = (self.operation)(input);
142        first == second
143    }
144
145    fn describe_failure(&self, input: &I) -> String {
146        format!("idempotent violated for input: {:?}", input)
147    }
148}
149
150/// Built-in property: commutative operations.
151pub struct CommutativeProperty<F> {
152    /// Binary operation to test.
153    operation: F,
154}
155
156impl<F, I, O> Property<(I, I)> for CommutativeProperty<F>
157where
158    F: Fn(&I, &I) -> O,
159    O: PartialEq + Debug,
160    I: Clone + Debug,
161{
162    fn name(&self) -> &str {
163        "commutative"
164    }
165
166    fn check(&self, (a, b): &(I, I)) -> bool {
167        let ab = (self.operation)(a, b);
168        let ba = (self.operation)(b, a);
169        ab == ba
170    }
171
172    fn describe_failure(&self, (a, b): &(I, I)) -> String {
173        format!("commutative violated for: ({:?}, {:?})", a, b)
174    }
175}
176
177/// Built-in property: associative operations.
178pub struct AssociativeProperty<F> {
179    /// Binary operation to test.
180    operation: F,
181}
182
183impl<F, I> Property<(I, I, I)> for AssociativeProperty<F>
184where
185    F: Fn(&I, &I) -> I,
186    I: PartialEq + Debug + Clone,
187{
188    fn name(&self) -> &str {
189        "associative"
190    }
191
192    fn check(&self, (a, b, c): &(I, I, I)) -> bool {
193        let ab = (self.operation)(a, b);
194        let ab_c = (self.operation)(&ab, c);
195
196        let bc = (self.operation)(b, c);
197        let a_bc = (self.operation)(a, &bc);
198
199        ab_c == a_bc
200    }
201
202    fn describe_failure(&self, (a, b, c): &(I, I, I)) -> String {
203        format!("associative violated for: ({:?}, {:?}, {:?})", a, b, c)
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_append_only_property() {
213        let prop = AppendOnlyProperty;
214
215        // Valid append
216        assert!(prop.check(&(vec![1, 2, 3], vec![1, 2, 3, 4, 5])));
217
218        // Invalid: data removed
219        assert!(!prop.check(&(vec![1, 2, 3], vec![1, 2])));
220
221        // Invalid: data modified
222        assert!(!prop.check(&(vec![1, 2, 3], vec![1, 9, 3, 4])));
223    }
224
225    #[test]
226    fn test_property_test_runner() {
227        let inputs = vec![
228            (vec![1u8], vec![1u8, 2]),
229            (vec![1u8, 2], vec![1u8, 2, 3]),
230            (vec![], vec![1u8]),
231        ];
232
233        let result = PropertyTest::new(AppendOnlyProperty, inputs.into_iter())
234            .with_iterations(10)
235            .run();
236
237        assert!(result.is_passed());
238    }
239
240    #[test]
241    fn test_property_failure() {
242        let inputs = vec![
243            (vec![1u8, 2, 3], vec![1u8, 2]), // This violates append-only
244        ];
245
246        let result = PropertyTest::new(AppendOnlyProperty, inputs.into_iter()).run();
247
248        assert!(result.is_failed());
249    }
250}