testingrustproperty-based-testingproptest

How can I keep the transformation of arbitrary proptest values out of the test case body?


I want to thoroughly test an implementation of the intersection of two BTreeSets. I can write:

use self::proptest::prelude::*;
proptest! {
    #[test]
    fn intersect_this(s1: BTreeSet<i32>, s2: BTreeSet<i32>) {
        // ...
    }
}

But this has poor code coverage, because the code is specialized in some cases that random sets are unlikely to hit. One of the special cases is sets whose ranges of elements are almost disjoint (one set has values <= x, the other set has values >= x). In Python with hypothesis (in which I'm a little less of a newbie), I'd write:

from hypothesis import given
from hypothesis.strategies import builds, integers, sets
from typing import Set

def touching_ranges(elements: Set[int], split: int):
    return {elt for elt in elements if elt < split}.union({split}), \
           {elt for elt in elements if elt > split}.union({split})

@given(builds(touching_ranges, sets(integers()), integers()))
def test_touching_ranges(sets):
    s1, s2 = sets
    assert len(s1.intersection(s2)) == 1

In Rust, I got no further than stuffing everything inside the body:

#[test]
fn touching(mut s1: BTreeSet<i32>, split: i32) {
    let mut s2 = s1.split_off(&split);
    s1.insert(split);
    s2.insert(split);
    prop_assert_eq!(s1.intersection(&s2).count(), 1);
}

How can I keep the transformation of arbitrary values out of the test case body? I couldn't understand any of the code samples I found regarding strategies and Stack Overflow has few proptest-related questions.


Solution

  • There is a built-in BTreeSetStrategy in proptest, so it is relatively straightforward:

    use proptest::prelude::*;
    use std::collections::BTreeSet;
    
    prop_compose! {
        fn touching_ranges()
                          (split: i32,
                           mut s1: BTreeSet<i32>)
                          -> (BTreeSet<i32>, BTreeSet<i32>)
        {
            let mut s2 = s1.split_off(&split);
            s1.insert(split);
            s2.insert(split);
    
            (s1, s2)
        }
    }
    
    proptest! {
        #[test]
        fn touching((s1, s2) in touching_ranges()) {
            assert_eq!(s1.intersection(&s2).count(), 1);
        }
    }
    

    Some syntax here is not vanilla Rust so it may need further explanation: