//! `proptest`-related features for `nalgebra` data structures. //! //! **This module is only available when the `proptest-support` feature is enabled in `nalgebra`**. //! //! `proptest` is a library for *property-based testing*. While similar to QuickCheck, //! which may be more familiar to some users, it has a more sophisticated design that //! provides users with automatic invariant-preserving shrinking. This means that when using //! `proptest`, you rarely need to write your own shrinkers - which is usually very difficult - //! and can instead get this "for free". Moreover, `proptest` does not rely on a canonical //! `Arbitrary` trait implementation like QuickCheck, though it does also provide this. For //! more information, check out the [proptest docs](https://docs.rs/proptest/0.10.1/proptest/) //! and the [proptest book](https://altsysrq.github.io/proptest-book/intro.html). //! //! This module provides users of `nalgebra` with tools to work with `nalgebra` types in //! `proptest` tests. At present, this integration is at an early stage, and only //! provides tools for generating matrices and vectors, and not any of the geometry types. //! There are essentially two ways of using this functionality: //! //! - Using the [matrix](fn.matrix.html) function to generate matrices with constraints //! on dimensions and elements. //! - Relying on the `Arbitrary` implementation of `MatrixMN`. //! //! The first variant is almost always preferred in practice. Read on to discover why. //! //! ### Using free function strategies //! //! In `proptest`, it is usually preferable to have free functions that generate *strategies*. //! Currently, the [matrix](fn.matrix.html) function fills this role. The analogous function for //! column vectors is [vector](fn.vector.html). Let's take a quick look at how it may be used: //! ```rust //! use nalgebra::proptest::matrix; //! use proptest::prelude::*; //! //! proptest! { //! # /* //! #[test] //! # */ //! fn my_test(a in matrix(-5 ..= 5, 2 ..= 4, 1..=4)) { //! // Generates matrices with elements in the range -5 ..= 5, rows in 2..=4 and //! // columns in 1..=4. //! } //! } //! //! # fn main() { my_test(); } //! ``` //! //! In the above example, we generate matrices with constraints on the elements, as well as the //! on the allowed dimensions. When a failing example is found, the resulting shrinking process //! will preserve these invariants. We can use this to compose more advanced strategies. //! For example, let's consider a toy example where we need to generate pairs of matrices //! with exactly 3 rows fixed at compile-time and the same number of columns, but we want the //! number of columns to vary. One way to do this is to use `proptest` combinators in combination //! with [matrix](fn.matrix.html) as follows: //! //! ```rust //! use nalgebra::{Dynamic, MatrixMN, U3}; //! use nalgebra::proptest::matrix; //! use proptest::prelude::*; //! //! type MyMatrix = MatrixMN; //! //! /// Returns a strategy for pairs of matrices with `U3` rows and the same number of //! /// columns. //! fn matrix_pairs() -> impl Strategy { //! matrix(-5 ..= 5, U3, 0 ..= 10) //! // We first generate the initial matrix `a`, and then depending on the concrete //! // instances of `a`, we pick a second matrix with the same number of columns //! .prop_flat_map(|a| { //! let b = matrix(-5 .. 5, U3, a.ncols()); //! // This returns a new tuple strategy where we keep `a` fixed while //! // the second item is a strategy that generates instances with the same //! // dimensions as `a` //! (Just(a), b) //! }) //! } //! //! proptest! { //! # /* //! #[test] //! # */ //! fn my_test((a, b) in matrix_pairs()) { //! // Let's double-check that the two matrices do indeed have the same number of //! // columns //! prop_assert_eq!(a.ncols(), b.ncols()); //! } //! } //! //! # fn main() { my_test(); } //! ``` //! //! ### The `Arbitrary` implementation //! //! If you don't care about the dimensions of matrices, you can write tests like these: //! //! ```rust //! use nalgebra::{DMatrix, DVector, Dynamic, Matrix3, MatrixMN, Vector3, U3}; //! use proptest::prelude::*; //! //! proptest! { //! # /* //! #[test] //! # */ //! fn test_dynamic(matrix: DMatrix) { //! // This will generate arbitrary instances of `DMatrix` and also attempt //! // to shrink/simplify them when test failures are encountered. //! } //! //! # /* //! #[test] //! # */ //! fn test_static_and_mixed(matrix: Matrix3, matrix2: MatrixMN) { //! // Test some property involving these matrices //! } //! //! # /* //! #[test] //! # */ //! fn test_vectors(fixed_size_vector: Vector3, dyn_vector: DVector) { //! // Test some property involving these vectors //! } //! } //! //! # fn main() { test_dynamic(); test_static_and_mixed(); test_vectors(); } //! ``` //! //! While this may be convenient, the default strategies for built-in types in `proptest` can //! generate *any* number, including integers large enough to easily lead to overflow when used in //! matrix operations, or even infinity or NaN values for floating-point types. Therefore //! `Arbitrary` is rarely the method of choice for writing property-based tests. //! //! ### Notes on shrinking //! //! Due to some limitations of the current implementation, shrinking takes place by first //! shrinking the matrix elements before trying to shrink the dimensions of the matrix. //! This unfortunately often leads to the fact that a large number of shrinking iterations //! are necessary to find a (nearly) minimal failing test case. As a workaround for this, //! you can increase the maximum number of shrinking iterations when debugging. To do this, //! simply set the `PROPTEST_MAX_SHRINK_ITERS` variable to a high number. For example: //! //! ```text //! PROPTEST_MAX_SHRINK_ITERS=100000 cargo test my_failing_test //! ``` use crate::allocator::Allocator; use crate::{DefaultAllocator, Dim, DimName, Dynamic, MatrixMN, Scalar, U1}; use proptest::arbitrary::Arbitrary; use proptest::collection::vec; use proptest::strategy::{BoxedStrategy, Just, NewTree, Strategy, ValueTree}; use proptest::test_runner::TestRunner; use std::ops::RangeInclusive; /// Parameters for arbitrary matrix generation. #[derive(Debug, Clone)] #[non_exhaustive] pub struct MatrixParameters { /// The range of rows that may be generated. pub rows: DimRange, /// The range of columns that may be generated. pub cols: DimRange, /// Parameters for the `Arbitrary` implementation of the scalar values. pub value_parameters: NParameters, } /// A range of allowed dimensions for use in generation of matrices. /// /// The `DimRange` type is used to encode the range of dimensions that can be used for generation /// of matrices with `proptest`. In most cases, you do not need to concern yourself with /// `DimRange` directly, as it supports conversion from other types such as `U3` or inclusive /// ranges such as `5 ..= 6`. The latter example corresponds to dimensions from (inclusive) /// `Dynamic::new(5)` to `Dynamic::new(6)` (inclusive). #[derive(Debug, Clone, PartialEq, Eq)] pub struct DimRange(RangeInclusive); impl DimRange { /// The lower bound for dimensions generated. pub fn lower_bound(&self) -> D { *self.0.start() } /// The upper bound for dimensions generated. pub fn upper_bound(&self) -> D { *self.0.end() } } impl From for DimRange { fn from(dim: D) -> Self { DimRange(dim..=dim) } } impl From> for DimRange { fn from(range: RangeInclusive) -> Self { DimRange(range) } } impl From> for DimRange { fn from(range: RangeInclusive) -> Self { DimRange::from(Dynamic::new(*range.start())..=Dynamic::new(*range.end())) } } impl DimRange { /// Converts the `DimRange` into an instance of `RangeInclusive`. pub fn to_range_inclusive(&self) -> RangeInclusive { self.lower_bound().value()..=self.upper_bound().value() } } impl From for DimRange { fn from(dim: usize) -> Self { DimRange::from(Dynamic::new(dim)) } } /// The default range used for Dynamic dimensions when generating arbitrary matrices. fn dynamic_dim_range() -> DimRange { DimRange::from(0..=6) } /// Create a strategy to generate matrices containing values drawn from the given strategy, /// with rows and columns in the provided ranges. /// /// ## Examples /// ``` /// use nalgebra::proptest::matrix; /// use nalgebra::{MatrixMN, U3, Dynamic}; /// use proptest::prelude::*; /// /// proptest! { /// # /* /// #[test] /// # */ /// fn my_test(a in matrix(0 .. 5i32, U3, 0 ..= 5)) { /// // Let's make sure we've got the correct type first /// let a: MatrixMN<_, U3, Dynamic> = a; /// prop_assert!(a.nrows() == 3); /// prop_assert!(a.ncols() <= 5); /// prop_assert!(a.iter().all(|x_ij| *x_ij >= 0 && *x_ij < 5)); /// } /// } /// /// # fn main() { my_test(); } /// ``` /// /// ## Limitations /// The current implementation has some limitations that lead to suboptimal shrinking behavior. /// See the [module-level documentation](index.html) for more. pub fn matrix( value_strategy: ScalarStrategy, rows: impl Into>, cols: impl Into>, ) -> MatrixStrategy where ScalarStrategy: Strategy + Clone + 'static, ScalarStrategy::Value: Scalar, R: Dim, C: Dim, DefaultAllocator: Allocator, { matrix_(value_strategy, rows.into(), cols.into()) } /// Same as `matrix`, but without the additional anonymous generic types fn matrix_( value_strategy: ScalarStrategy, rows: DimRange, cols: DimRange, ) -> MatrixStrategy where ScalarStrategy: Strategy + Clone + 'static, ScalarStrategy::Value: Scalar, R: Dim, C: Dim, DefaultAllocator: Allocator, { let nrows = rows.lower_bound().value()..=rows.upper_bound().value(); let ncols = cols.lower_bound().value()..=cols.upper_bound().value(); // Even though we can use this function to generate fixed-size matrices, // we currently generate all matrices with heap allocated Vec data. // TODO: Avoid heap allocation for fixed-size matrices. // Doing this *properly* would probably require us to implement a custom // strategy and valuetree with custom shrinking logic, which is not trivial // Perhaps more problematic, however, is the poor shrinking behavior the current setup leads to. // Shrinking in proptest basically happens in "reverse" of the combinators, so // by first generating the dimensions and then the elements, we get shrinking that first // tries to completely shrink the individual elements before trying to reduce the dimension. // This is clearly the opposite of what we want. I can't find any good way around this // short of writing our own custom value tree, which we should probably do at some point. // TODO: Custom implementation of value tree for better shrinking behavior. let strategy = nrows .prop_flat_map(move |nrows| (Just(nrows), ncols.clone())) .prop_flat_map(move |(nrows, ncols)| { ( Just(nrows), Just(ncols), vec(value_strategy.clone(), nrows * ncols), ) }) .prop_map(|(nrows, ncols, values)| { // Note: R/C::from_usize will panic if nrows/ncols does not fit in the dimension type. // However, this should never fail, because we should only be generating // this stuff in the first place MatrixMN::from_iterator_generic(R::from_usize(nrows), C::from_usize(ncols), values) }) .boxed(); MatrixStrategy { strategy } } /// Create a strategy to generate column vectors containing values drawn from the given strategy, /// with length in the provided range. /// /// This is a convenience function for calling /// [matrix(value_strategy, length, U1)](fn.matrix.html) and should /// be used when you only want to generate column vectors, as it's simpler and makes the intent /// clear. pub fn vector( value_strategy: ScalarStrategy, length: impl Into>, ) -> MatrixStrategy where ScalarStrategy: Strategy + Clone + 'static, ScalarStrategy::Value: Scalar, D: Dim, DefaultAllocator: Allocator, { matrix_(value_strategy, length.into(), U1.into()) } impl Default for MatrixParameters where NParameters: Default, R: DimName, C: DimName, { fn default() -> Self { Self { rows: DimRange::from(R::name()), cols: DimRange::from(C::name()), value_parameters: NParameters::default(), } } } impl Default for MatrixParameters where NParameters: Default, R: DimName, { fn default() -> Self { Self { rows: DimRange::from(R::name()), cols: dynamic_dim_range(), value_parameters: NParameters::default(), } } } impl Default for MatrixParameters where NParameters: Default, C: DimName, { fn default() -> Self { Self { rows: dynamic_dim_range(), cols: DimRange::from(C::name()), value_parameters: NParameters::default(), } } } impl Default for MatrixParameters where NParameters: Default, { fn default() -> Self { Self { rows: dynamic_dim_range(), cols: dynamic_dim_range(), value_parameters: NParameters::default(), } } } impl Arbitrary for MatrixMN where N: Scalar + Arbitrary, ::Strategy: Clone, R: Dim, C: Dim, MatrixParameters: Default, DefaultAllocator: Allocator, { type Parameters = MatrixParameters; fn arbitrary_with(args: Self::Parameters) -> Self::Strategy { let value_strategy = N::arbitrary_with(args.value_parameters); matrix(value_strategy, args.rows, args.cols) } type Strategy = MatrixStrategy; } /// A strategy for generating matrices. #[derive(Debug, Clone)] pub struct MatrixStrategy where NStrategy: Strategy, NStrategy::Value: Scalar, DefaultAllocator: Allocator, { // For now we only internally hold a boxed strategy. The reason for introducing this // separate wrapper struct is so that we can replace the strategy logic with custom logic // later down the road without introducing significant breaking changes strategy: BoxedStrategy>, } impl Strategy for MatrixStrategy where NStrategy: Strategy, NStrategy::Value: Scalar, R: Dim, C: Dim, DefaultAllocator: Allocator, { type Tree = MatrixValueTree; type Value = MatrixMN; fn new_tree(&self, runner: &mut TestRunner) -> NewTree { let underlying_tree = self.strategy.new_tree(runner)?; Ok(MatrixValueTree { value_tree: underlying_tree, }) } } /// A value tree for matrices. pub struct MatrixValueTree where N: Scalar, R: Dim, C: Dim, DefaultAllocator: Allocator, { // For now we only wrap a boxed value tree. The reason for wrapping is that this allows us // to swap out the value tree logic down the road without significant breaking changes. value_tree: Box>>, } impl ValueTree for MatrixValueTree where N: Scalar, R: Dim, C: Dim, DefaultAllocator: Allocator, { type Value = MatrixMN; fn current(&self) -> Self::Value { self.value_tree.current() } fn simplify(&mut self) -> bool { self.value_tree.simplify() } fn complicate(&mut self) -> bool { self.value_tree.complicate() } }