3

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.

Unapiedra
  • 15,037
  • 12
  • 64
  • 93
Stein
  • 1,558
  • 23
  • 30

1 Answers1

4

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:

  • Inside the proptest! macro, the tests are normal Rust functions except they also have access to the in Strategy syntax in order to generate inputs.
  • The Proptest strategies are either built-in or user defined. One way to define a strategy is inside a prop_compose! macro. Again, this is a normal Rust function except it can have two argument lists. The first argument list is the usual input; the second one can use the in Strategy syntax and arguments from the first. The return type indicates the type of value being generated. In this case, a tuple of two BTreeSets.
  • As you may have guessed, the Proptest crate comes with Strategy implementations for tuples, so a tuple of types that implement Strategy is itself a Strategy. That's why the function touching_ranges can be used as one.
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
edwardw
  • 12,652
  • 3
  • 40
  • 51