hegeltest 0.12.7

Property-based testing for Rust, built on Hypothesis
Documentation
// Collection schema interpreters: list, dict, tuple, one_of, sampled_from.

use crate::cbor_utils::{as_bool, as_u64, map_get};
use crate::native::core::{ManyState, NativeTestCase, StopTest};
use ciborium::Value;

use super::{cbor_to_i128, interpret_schema, many_more, many_reject};

pub(super) fn interpret_tuple(ntc: &mut NativeTestCase, schema: &Value) -> Result<Value, StopTest> {
    let elements = match map_get(schema, "elements") {
        Some(Value::Array(arr)) => arr,
        _ => panic!("tuple schema must have elements array"),
    };
    let mut results = Vec::with_capacity(elements.len());
    for element_schema in elements {
        results.push(interpret_schema(ntc, element_schema)?);
    }
    Ok(Value::Array(results))
}

pub(super) fn interpret_one_of(
    ntc: &mut NativeTestCase,
    schema: &Value,
) -> Result<Value, StopTest> {
    let generators = match map_get(schema, "generators") {
        Some(Value::Array(arr)) => arr,
        _ => panic!("one_of schema must have generators array"),
    };
    assert!(
        !generators.is_empty(),
        "one_of schema must have at least one generator"
    );
    let idx = ntc.draw_integer(0, generators.len() as i128 - 1)?;
    let value = interpret_schema(ntc, &generators[idx as usize])?;
    Ok(Value::Array(vec![
        Value::Integer((idx as i64).into()),
        value,
    ]))
}

pub(super) fn interpret_sampled_from(
    ntc: &mut NativeTestCase,
    schema: &Value,
) -> Result<Value, StopTest> {
    let values = match map_get(schema, "values") {
        Some(Value::Array(arr)) => arr,
        _ => panic!("sampled_from schema must have values array"),
    };
    assert!(
        !values.is_empty(),
        "sampled_from schema must have at least one value"
    );
    let idx = ntc.draw_integer(0, values.len() as i128 - 1)?;
    Ok(encode_schema_value(&values[idx as usize]))
}

pub(super) fn interpret_list(ntc: &mut NativeTestCase, schema: &Value) -> Result<Value, StopTest> {
    let element_schema = map_get(schema, "elements").expect("list schema must have elements");
    let min_size = map_get(schema, "min_size").and_then(as_u64).unwrap_or(0) as usize;
    let max_size = map_get(schema, "max_size")
        .and_then(as_u64)
        .map(|n| n as usize);
    let unique = map_get(schema, "unique").and_then(as_bool).unwrap_or(false);

    if unique {
        if let Some((min_val, max_val)) = bounded_integer_range(element_schema) {
            let range_size = (max_val - min_val + 1) as usize;
            return interpret_unique_integer_list(ntc, min_size, max_size, min_val, range_size);
        }
    }

    let mut state = ManyState::new(min_size, max_size);
    let mut results: Vec<Value> = Vec::new();

    loop {
        if !many_more(ntc, &mut state)? {
            break;
        }
        let element = interpret_schema(ntc, element_schema)?;
        if unique && results.iter().any(|existing| existing == &element) {
            many_reject(ntc, &mut state)?;
            continue;
        }
        results.push(element);
    }

    Ok(Value::Array(results))
}

/// Pool-based unique list generation for bounded integer ranges.
/// Port of Hypothesis's UniqueSampledListStrategy: draw indices into a
/// shrinking pool of remaining values, avoiding the coupon-collector problem.
fn interpret_unique_integer_list(
    ntc: &mut NativeTestCase,
    min_size: usize,
    max_size: Option<usize>,
    min_val: i128,
    range_size: usize,
) -> Result<Value, StopTest> {
    let effective_max = max_size.map_or(range_size, |m| m.min(range_size));
    let mut state = ManyState::new(min_size, Some(effective_max));
    let mut remaining: Vec<i128> = (min_val..min_val + range_size as i128).collect();
    let mut results = Vec::new();

    loop {
        if remaining.is_empty() || !many_more(ntc, &mut state)? {
            break;
        }
        let j = ntc.draw_integer(0, remaining.len() as i128 - 1)? as usize;
        let value = remaining.remove(j);
        results.push(Value::Integer((value as i64).into()));
    }

    Ok(Value::Array(results))
}

fn bounded_integer_range(schema: &Value) -> Option<(i128, i128)> {
    use crate::cbor_utils::as_text;
    let schema_type = map_get(schema, "type").and_then(as_text)?;
    if schema_type != "integer" {
        return None;
    }
    let min_val = cbor_to_i128(map_get(schema, "min_value")?);
    let max_val = cbor_to_i128(map_get(schema, "max_value")?);
    if !(1..=10_000).contains(&(max_val - min_val + 1)) {
        return None;
    }
    Some((min_val, max_val))
}

pub(super) fn interpret_dict(ntc: &mut NativeTestCase, schema: &Value) -> Result<Value, StopTest> {
    let key_schema = map_get(schema, "keys").expect("dict schema must have keys");
    let val_schema = map_get(schema, "values").expect("dict schema must have values");
    let min_size = map_get(schema, "min_size").and_then(as_u64).unwrap_or(0) as usize;
    let max_size = map_get(schema, "max_size")
        .and_then(as_u64)
        .map(|n| n as usize);

    let mut state = ManyState::new(min_size, max_size);
    let mut pairs: Vec<Value> = Vec::new();
    let mut keys: Vec<Value> = Vec::new();

    loop {
        if !many_more(ntc, &mut state)? {
            break;
        }
        let key = interpret_schema(ntc, key_schema)?;
        if keys.iter().any(|existing| existing == &key) {
            many_reject(ntc, &mut state)?;
            continue;
        }
        let value = interpret_schema(ntc, val_schema)?;
        keys.push(key.clone());
        pairs.push(Value::Array(vec![key, value]));
    }

    Ok(Value::Array(pairs))
}

/// Encode a schema value for transport back to the generator.
///
/// Mirrors hegel-core's `_encode_value`: text strings are wrapped in
/// CBOR tag 91 (HEGEL_STRING_TAG) so they can be deserialized by `HegelValue`.
fn encode_schema_value(value: &Value) -> Value {
    match value {
        Value::Text(s) => Value::Tag(91, Box::new(Value::Bytes(s.as_bytes().to_vec()))),
        other => other.clone(),
    }
}

#[cfg(test)]
#[path = "../../../tests/embedded/native/schema/collections_tests.rs"]
mod tests;