Skip to main content

windjammer_runtime/
property.rs

1//! Property-based testing utilities for Windjammer
2//!
3//! Provides simple property-based testing without external dependencies.
4//! For more advanced property testing, integrate proptest or quickcheck.
5
6use std::fmt::Debug;
7
8/// Run a property test with random generated inputs
9///
10/// # Example
11/// ```
12/// use windjammer_runtime::property::property_test;
13///
14/// property_test(100, || {
15///     let a = rand::random::<i32>() % 1000;
16///     let b = rand::random::<i32>() % 1000;
17///     // Test property: addition is commutative
18///     assert_eq!(a + b, b + a);
19/// });
20/// ```
21pub fn property_test<F: Fn()>(iterations: usize, f: F) {
22    for i in 0..iterations {
23        // Run the property test
24        // If it panics, the test fails
25        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(&f)) {
26            Ok(_) => {}
27            Err(e) => {
28                panic!(
29                    "Property test failed on iteration {}/{}: {:?}",
30                    i + 1,
31                    iterations,
32                    e
33                );
34            }
35        }
36    }
37}
38
39/// Test a property with a specific generator function
40///
41/// # Example
42/// ```
43/// use windjammer_runtime::property::property_test_with_gen;
44///
45/// property_test_with_gen(100, || rand::random::<u32>() % 100, |value| {
46///     // Property: all generated values should be < 100
47///     assert!(value < 100);
48/// });
49/// ```
50pub fn property_test_with_gen<T, G, F>(iterations: usize, gen: G, property: F)
51where
52    G: Fn() -> T,
53    F: Fn(T),
54{
55    for i in 0..iterations {
56        let value = gen();
57        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| property(value))) {
58            Ok(_) => {}
59            Err(e) => {
60                panic!(
61                    "Property test failed on iteration {}/{}: {:?}",
62                    i + 1,
63                    iterations,
64                    e
65                );
66            }
67        }
68    }
69}
70
71/// Alias for `property_test_with_gen` to match the naming convention of gen2/gen3
72/// This is used by the Windjammer compiler's code generation
73pub fn property_test_with_gen1<T, G, F>(iterations: usize, gen: G, property: F)
74where
75    G: Fn() -> T,
76    F: Fn(T),
77{
78    property_test_with_gen(iterations, gen, property)
79}
80
81/// Test a property with two generated inputs
82///
83/// # Example
84/// ```
85/// use windjammer_runtime::property::property_test_with_gen2;
86///
87/// property_test_with_gen2(
88///     100,
89///     || rand::random::<i32>() % 1000,
90///     || rand::random::<i32>() % 1000,
91///     |a, b| {
92///         // Property: addition is commutative
93///         assert_eq!(a + b, b + a);
94///     }
95/// );
96/// ```
97pub fn property_test_with_gen2<T1, T2, G1, G2, F>(
98    iterations: usize,
99    gen1: G1,
100    gen2: G2,
101    property: F,
102) where
103    G1: Fn() -> T1,
104    G2: Fn() -> T2,
105    F: Fn(T1, T2),
106{
107    for i in 0..iterations {
108        let val1 = gen1();
109        let val2 = gen2();
110        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| property(val1, val2))) {
111            Ok(_) => {}
112            Err(e) => {
113                panic!(
114                    "Property test failed on iteration {}/{}: {:?}",
115                    i + 1,
116                    iterations,
117                    e
118                );
119            }
120        }
121    }
122}
123
124/// Test a property with three generated inputs
125pub fn property_test_with_gen3<T1, T2, T3, G1, G2, G3, F>(
126    iterations: usize,
127    gen1: G1,
128    gen2: G2,
129    gen3: G3,
130    property: F,
131) where
132    G1: Fn() -> T1,
133    G2: Fn() -> T2,
134    G3: Fn() -> T3,
135    F: Fn(T1, T2, T3),
136{
137    for i in 0..iterations {
138        let val1 = gen1();
139        let val2 = gen2();
140        let val3 = gen3();
141        match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| property(val1, val2, val3)))
142        {
143            Ok(_) => {}
144            Err(e) => {
145                panic!(
146                    "Property test failed on iteration {}/{}: {:?}",
147                    i + 1,
148                    iterations,
149                    e
150                );
151            }
152        }
153    }
154}
155
156/// Shrink a value to find minimal failing case
157/// This is a simplified shrinking strategy
158pub fn shrink_int(value: i64) -> Vec<i64> {
159    let mut shrinks = vec![0];
160    let mut current = value.abs() / 2;
161    while current > 0 {
162        shrinks.push(current);
163        shrinks.push(-current);
164        current /= 2;
165    }
166    shrinks
167}
168
169/// Find the minimal failing input using shrinking
170///
171/// # Example
172/// ```
173/// use windjammer_runtime::property::{find_minimal_failing, shrink_int};
174///
175/// let minimal = find_minimal_failing(
176///     1000,
177///     shrink_int,
178///     |x| x < 100  // Property fails for x >= 100
179/// );
180///
181/// if let Some(min) = minimal {
182///     println!("Minimal failing value: {}", min);
183/// }
184/// ```
185pub fn find_minimal_failing<T: Copy + Debug, F, S>(initial: T, shrink: S, property: F) -> Option<T>
186where
187    F: Fn(T) -> bool,
188    S: Fn(T) -> Vec<T>,
189{
190    // Check if initial value fails
191    if property(initial) {
192        return None;
193    }
194
195    let mut current_failure = initial;
196
197    // Try to shrink to a simpler failing case
198    for shrunk in shrink(current_failure) {
199        if !property(shrunk) {
200            current_failure = shrunk;
201        }
202    }
203
204    Some(current_failure)
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_property_test_passes() {
213        property_test(10, || {
214            // This property should always hold
215            let a = 5;
216            let b = 3;
217            assert_eq!(a + b, b + a);
218        });
219    }
220
221    #[test]
222    #[should_panic(expected = "Property test failed")]
223    fn test_property_test_fails() {
224        property_test(10, || {
225            panic!("Intentional failure");
226        });
227    }
228
229    #[test]
230    fn test_property_test_with_gen() {
231        property_test_with_gen(
232            10,
233            || 42,
234            |value| {
235                assert_eq!(value, 42);
236            },
237        );
238    }
239
240    #[test]
241    fn test_property_test_with_gen2() {
242        property_test_with_gen2(
243            10,
244            || 1,
245            || 2,
246            |a, b| {
247                assert_eq!(a + b, 3);
248            },
249        );
250    }
251
252    #[test]
253    fn test_property_test_with_gen3() {
254        property_test_with_gen3(
255            10,
256            || 1,
257            || 2,
258            || 3,
259            |a, b, c| {
260                assert_eq!(a + b + c, 6);
261            },
262        );
263    }
264
265    #[test]
266    fn test_shrink_int() {
267        let shrinks = shrink_int(100);
268        assert!(shrinks.contains(&0));
269        assert!(shrinks.contains(&50));
270        assert!(shrinks.contains(&-50));
271        assert!(shrinks.contains(&25));
272    }
273
274    #[test]
275    fn test_find_minimal_failing() {
276        let minimal = find_minimal_failing(
277            1000,
278            shrink_int,
279            |x| x < 100, // Property fails for x >= 100
280        );
281
282        assert!(minimal.is_some());
283        let min = minimal.unwrap();
284        assert!(min >= 100); // Should find a value >= 100
285    }
286}