use core::num::NonZeroU64;
use std::sync::Arc;
use proptest::prelude::*;
use proptest::strategy::{BoxedStrategy, Just, Strategy};
use smartstring::alias::String as SmartString;
use crate::domain::{IdDomain, KeyDomain};
use crate::id::Id;
use crate::key::Key;
pub trait ProptestKeyDomain: KeyDomain {
fn proptest_strategy() -> Option<BoxedStrategy<Key<Self>>>
where
Self: Sized,
{
None
}
}
impl<D: IdDomain> proptest::arbitrary::Arbitrary for Id<D> {
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: ()) -> BoxedStrategy<Self> {
any::<u64>()
.prop_map(|raw| Id::from_non_zero(NonZeroU64::new(raw | 1).expect("raw | 1 != 0")))
.boxed()
}
}
#[cfg(feature = "uuid")]
impl<D: crate::domain::UuidDomain> proptest::arbitrary::Arbitrary
for crate::uuid::Uuid<D>
{
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: ()) -> BoxedStrategy<Self> {
any::<[u8; 16]>()
.prop_map(|bytes| Self::from_bytes(bytes))
.boxed()
}
}
#[cfg(feature = "ulid")]
impl<D: crate::domain::UlidDomain> proptest::arbitrary::Arbitrary
for crate::ulid::Ulid<D>
{
type Parameters = ();
type Strategy = BoxedStrategy<Self>;
fn arbitrary_with(_: ()) -> BoxedStrategy<Self> {
any::<[u8; 16]>()
.prop_map(|bytes| Self::from_bytes(bytes))
.boxed()
}
}
fn build_key_from_indices<D: KeyDomain>(
alphabet: &[char],
start_chars: &[char],
indices: &[usize],
) -> Option<Key<D>> {
if indices.is_empty() {
return None;
}
let length = indices.len();
let mut result = SmartString::new();
let mut prev: Option<char> = None;
for (pos, &idx) in indices.iter().enumerate() {
let is_last = pos == length - 1;
let candidates: Vec<char> = match pos {
0 if is_last => start_chars
.iter()
.copied()
.filter(|&c| D::allowed_end_character(c))
.collect(),
0 => start_chars.to_vec(),
_ => {
let p = prev.expect("prev is set after position 0");
alphabet
.iter()
.copied()
.filter(|&c| D::allowed_consecutive_characters(p, c))
.filter(|&c| !is_last || D::allowed_end_character(c))
.collect()
}
};
if candidates.is_empty() {
return None;
}
let c = candidates[idx % candidates.len()];
result.push(c);
prev = Some(c);
}
Key::new(result.as_str()).ok()
}
fn key_constructive_strategy<D: ProptestKeyDomain>() -> BoxedStrategy<Key<D>>
where
Key<D>: core::fmt::Debug,
{
let min_len = D::min_length().max(1);
let max_len = D::MAX_LENGTH.max(min_len);
let alphabet: Arc<Vec<char>> = Arc::new(
(' '..='~')
.filter(|&c| D::allowed_characters(c))
.collect(),
);
let start_chars: Arc<Vec<char>> = Arc::new(
alphabet
.iter()
.copied()
.filter(|&c| D::allowed_start_character(c))
.collect(),
);
(min_len..=max_len)
.prop_flat_map(move |len| {
let a = Arc::clone(&alphabet);
let s = Arc::clone(&start_chars);
proptest::collection::vec(any::<usize>(), len).prop_map(move |indices| {
build_key_from_indices::<D>(&a, &s, &indices)
})
})
.prop_filter_map("non-empty candidate set", |opt| opt)
.boxed()
}
impl<D: ProptestKeyDomain> proptest::arbitrary::Arbitrary for Key<D>
where
Key<D>: core::fmt::Debug,
{
type Parameters = ();
type Strategy = BoxedStrategy<Key<D>>;
fn arbitrary_with(_: ()) -> BoxedStrategy<Key<D>> {
if let Some(s) = D::proptest_strategy() {
return s;
}
if D::HAS_CUSTOM_VALIDATION {
let examples = D::examples();
if !examples.is_empty() {
let mut all_strats: Vec<BoxedStrategy<Key<D>>> = examples
.iter()
.map(|&s| {
let key = Key::from(SmartString::from(s));
Just(key).boxed()
})
.collect();
all_strats.push(key_constructive_strategy::<D>());
return proptest::strategy::Union::new(all_strats).boxed();
}
}
key_constructive_strategy::<D>()
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use crate::domain::{Domain, KeyDomain};
use crate::key::Key;
use super::ProptestKeyDomain;
#[derive(Debug, Clone)]
struct AlphaLowerDomain;
impl Domain for AlphaLowerDomain {
const DOMAIN_NAME: &'static str = "alpha_lower";
}
impl KeyDomain for AlphaLowerDomain {
const MAX_LENGTH: usize = 12;
fn allowed_characters(c: char) -> bool {
c.is_ascii_lowercase()
}
}
impl ProptestKeyDomain for AlphaLowerDomain {}
proptest! {
#[test]
fn key_strategy_produces_valid_keys(key in any::<Key<AlphaLowerDomain>>()) {
prop_assert!(Key::<AlphaLowerDomain>::new(key.as_str()).is_ok());
}
#[test]
fn key_strategy_respects_length_bounds(key in any::<Key<AlphaLowerDomain>>()) {
let len = key.len();
prop_assert!(
len >= AlphaLowerDomain::min_length() && len <= AlphaLowerDomain::MAX_LENGTH,
"key length {len} out of bounds"
);
}
}
#[test]
fn examples_domain_path2_runs_without_panic() {
#[derive(Debug, Clone)]
struct ExDomain;
impl Domain for ExDomain {
const DOMAIN_NAME: &'static str = "ex";
}
impl KeyDomain for ExDomain {
const MAX_LENGTH: usize = 16;
const HAS_CUSTOM_VALIDATION: bool = true;
fn examples() -> &'static [&'static str] {
&["alpha", "beta", "gamma"]
}
fn allowed_characters(c: char) -> bool {
c.is_ascii_lowercase()
}
}
impl ProptestKeyDomain for ExDomain {}
let mut runner = proptest::test_runner::TestRunner::default();
let strategy = any::<Key<ExDomain>>();
let valid_examples: &[&str] = ExDomain::examples();
let mut from_examples = 0usize;
let mut from_constructive = 0usize;
for _ in 0..50 {
let tree = strategy.new_tree(&mut runner).unwrap();
let key = tree.current();
assert!(
Key::<ExDomain>::new(key.as_str()).is_ok(),
"generated key failed validation: {key:?}"
);
if valid_examples.contains(&key.as_str()) {
from_examples += 1;
} else {
from_constructive += 1;
}
}
assert!(from_examples > 0, "no keys from examples");
let _ = from_constructive; }
}