Implementing a Property-Based Testing Framework in Rust
Property-based testing is a powerful technique for verifying the correctness of your code. Instead of testing specific input-output pairs, you define properties that should hold true for all valid inputs. A property-based testing framework then generates a wide range of random inputs to try and falsify these properties. This challenge asks you to build a simplified version of a property-based testing framework in Rust, similar to the popular quickcheck crate.
Problem Description
Your task is to create a Rust macro and associated helper functions that allow users to define properties and run tests against them. The framework should be able to generate random values for primitive types and common Rust collections, and then use these generated values to execute user-defined test functions.
Key Requirements:
prop_test!Macro: Create a macro namedprop_test!that takes a closure as an argument. This closure represents the property to be tested.- Type Generation: The framework must be able to generate random values for common primitive types (e.g.,
i32,u64,f64,bool,char) and basic collections likeVec<T>whereTis also generatable. - Input Generation: The macro should automatically infer the types of the arguments expected by the property closure and generate random values for them.
- Property Execution: The macro should call the provided closure multiple times (e.g., 100 times) with different randomly generated inputs.
- Failure Detection: If the closure ever returns
falseor panics, the test should be considered failed. The framework should report the failing input. - Success Indication: If the closure returns
truefor all generated inputs, the test should be considered successful.
Expected Behavior:
When prop_test!(|a: i32, b: String| { ... }) is used, the macro should:
- Generate random
i32andStringvalues. - Call the closure with these values repeatedly.
- If
closure(random_i32, random_string)returnsfalseor panics, report "Test failed for input: i32={}, String='{}'". - If all 100 (or configurable number) attempts succeed, report "Test passed!".
Edge Cases to Consider:
- Empty collections (
Vec::new()). - Strings with various characters, including empty strings.
- Integer overflow/underflow (though not strictly required to handle explicitly, the random generation should cover values near limits).
- Closures that might panic.
Examples
Example 1: Testing Sorting
Let's say we want to test a hypothetical my_sort function. A property could be that a sorted vector is always equal to a vector sorted by Rust's standard sort method.
// Assume 'my_sort' function exists and sorts a Vec<i32> in place.
// For this challenge, we'll simulate it returning a sorted Vec.
fn my_sort(mut vec: Vec<i32>) -> Vec<i32> {
vec.sort();
vec
}
prop_test!(|mut input_vec: Vec<i32>| {
let mut expected_vec = input_vec.clone();
expected_vec.sort();
my_sort(input_vec.clone()) == expected_vec
});
Output (if test passes):
Test passed!
Explanation: The prop_test! macro generates random Vec<i32>. For each generated vector, it sorts it using my_sort and compares it to a version sorted by the standard sort method. If they ever differ, the test fails. If they match for all generated vectors, the test passes.
Example 2: Testing String Reversal
Property: Reversing a string twice should result in the original string.
fn reverse_string(s: &str) -> String {
s.chars().rev().collect()
}
prop_test!(|s: String| {
reverse_string(&reverse_string(&s)) == s
});
Output (if test passes):
Test passed!
Explanation: The macro generates random strings. It then reverses each string twice and checks if it equals the original string.
Example 3: Integer Addition Commutativity
Property: a + b should equal b + a for integers.
prop_test!(|a: i32, b: i32| {
a.checked_add(b) == b.checked_add(a)
});
Output (if test passes):
Test passed!
Explanation: Generates random i32 values for a and b and verifies that addition is commutative. checked_add is used to avoid panics on overflow, allowing the property to be tested for all possible i32 values without causing test failures due to overflow panics.
Constraints
- The macro and helper code must be written in Rust.
- The framework should support at least
i32,u64,f64,bool,char, andVec<T>(whereTis a supported type). - The framework should run at least 100 test cases per property.
- Performance of the generation and testing itself should be reasonably efficient, but perfect optimization is not the primary goal. The focus is on functionality.
- The generated values should cover a good range of possibilities for each type.
Notes
- You will likely need to create a trait to define how to generate random values for different types.
- Consider how to handle different argument counts in the closure.
- The
randcrate is a good starting point for random number generation in Rust. - Think about how to report failures clearly, including the specific values that caused the failure.
- For
Vec<T>, ensure it can generate vectors of varying lengths, including empty ones. - You might need to implement custom logic for generating strings.