autumn-plugin-rocksdb 0.1.0

Autumn plugin: a RocksDB key-value store, cache and session store for Autumn apps.
use proptest::prelude::*;

use super::*;

fn range(
    prefix: Option<&[u8]>,
    start: Option<&[u8]>,
    end: Option<&[u8]>,
    after: Option<&[u8]>,
) -> Range {
    Range {
        prefix: prefix.map(<[u8]>::to_vec),
        start: start.map(<[u8]>::to_vec),
        end: end.map(<[u8]>::to_vec),
        after: after.map(<[u8]>::to_vec),
    }
}

#[test]
fn prefix_end_increments_the_last_byte() {
    assert_eq!(prefix_end(b"user:"), Some(b"user;".to_vec()));
    assert_eq!(prefix_end(&[1, 2, 3]), Some(vec![1, 2, 4]));
}

#[test]
fn prefix_end_drops_trailing_ff_bytes() {
    assert_eq!(prefix_end(&[1, 0xFF, 0xFF]), Some(vec![2]));
    assert_eq!(prefix_end(&[0, 0xFE, 0xFF]), Some(vec![0, 0xFF]));
}

#[test]
fn prefix_end_has_no_end_for_empty_and_ff_prefixes() {
    assert_eq!(prefix_end(b""), None);
    assert_eq!(prefix_end(&[0xFF, 0xFF]), None);
}

#[test]
fn no_conditions_give_all_keys() {
    let bounds = Bounds::new(&Range::default());
    assert_eq!(bounds.lower, b"");
    assert_eq!(bounds.upper, None);
    assert!(!bounds.is_empty());
    assert!(bounds.contains(b""));
    assert!(bounds.contains(&[0xFF, 0xFF]));
}

#[test]
fn a_prefix_gives_the_prefix_range() {
    let bounds = Bounds::new(&range(Some(b"b"), None, None, None));
    assert_eq!(bounds.lower, b"b");
    assert_eq!(bounds.upper, Some(b"c".to_vec()));
    assert!(bounds.contains(b"b"));
    assert!(bounds.contains(b"b\xFF"));
    assert!(!bounds.contains(b"a"));
    assert!(!bounds.contains(b"c"));
}

#[test]
fn start_is_inclusive_and_end_is_exclusive() {
    let bounds = Bounds::new(&range(None, Some(b"b"), Some(b"d"), None));
    assert!(!bounds.contains(b"a"));
    assert!(bounds.contains(b"b"));
    assert!(bounds.contains(b"c"));
    assert!(!bounds.contains(b"d"));
}

#[test]
fn the_cursor_is_exclusive() {
    let bounds = Bounds::new(&range(None, None, None, Some(b"b")));
    assert_eq!(bounds.lower, b"b\0");
    assert!(!bounds.contains(b"b"));
    assert!(bounds.contains(b"b\0"));
    assert!(bounds.contains(b"c"));
}

#[test]
fn the_largest_lower_and_the_smallest_upper_win() {
    let bounds = Bounds::new(&range(Some(b"k"), Some(b"k5"), Some(b"k8"), Some(b"k3")));
    assert_eq!(bounds.lower, b"k5");
    assert_eq!(bounds.upper, Some(b"k8".to_vec()));
    let bounds = Bounds::new(&range(Some(b"k"), Some(b"a"), Some(b"z"), Some(b"k7")));
    assert_eq!(bounds.lower, b"k7\0");
    assert_eq!(bounds.upper, Some(b"l".to_vec()));
}

#[test]
fn bounds_with_no_keys_are_empty() {
    assert!(Bounds::new(&range(None, Some(b"b"), Some(b"b"), None)).is_empty());
    assert!(Bounds::new(&range(None, Some(b"c"), Some(b"b"), None)).is_empty());
    assert!(Bounds::new(&range(Some(b"a"), Some(b"b"), None, None)).is_empty());
    assert!(!Bounds::new(&range(None, Some(b"b"), Some(b"b\0"), None)).is_empty());
}

fn small_key() -> impl Strategy<Value = Vec<u8>> {
    proptest::collection::vec(
        prop_oneof![Just(0_u8), Just(1), Just(0xFE), Just(0xFF), any::<u8>()],
        0..5,
    )
}

proptest! {
    #[test]
    fn prefix_end_bounds_each_key_with_the_prefix(prefix in small_key(), tail in small_key()) {
        let mut key = prefix.clone();
        key.extend_from_slice(&tail);
        if let Some(end) = prefix_end(&prefix) {
            prop_assert!(key < end);
            prop_assert!(prefix < end);
        }
    }

    #[test]
    fn contains_matches_each_condition(
        prefix in proptest::option::of(small_key()),
        start in proptest::option::of(small_key()),
        end in proptest::option::of(small_key()),
        after in proptest::option::of(small_key()),
        key in small_key(),
    ) {
        let range = Range { prefix: prefix.clone(), start: start.clone(), end: end.clone(), after: after.clone() };
        let bounds = Bounds::new(&range);
        let expected = prefix.as_ref().is_none_or(|p| key.starts_with(p))
            && start.as_ref().is_none_or(|s| key >= *s)
            && end.as_ref().is_none_or(|e| key < *e)
            && after.as_ref().is_none_or(|a| key > *a);
        prop_assert_eq!(bounds.contains(&key), expected);
        if expected {
            prop_assert!(!bounds.is_empty());
        }
    }
}