cqlite-core 0.11.0

Core engine for CQLite — read Apache Cassandra 5.0 SSTables locally without a cluster
Documentation
//! Property-based tests for collection and UDT parsing correctness (Issue #61)
//!
//! These tests validate:
//! - Random nesting of collections (lists of maps, maps of lists, etc.)
//! - Null injection at arbitrary depths
//! - Full buffer consumption at all levels
//! - Deterministic decoding with provided schemas

use crate::{
    parser::types::{parse_list_with_schema, parse_map_with_schema, CqlTypeId},
    parser::vint::encode_vint,
    schema::CqlType,
    types::Value,
};
use proptest::prelude::*;

// ============================================================================
// Property Test Generators
// ============================================================================

/// Generate a simple CQL value of a given type
fn arb_simple_value(type_id: CqlTypeId) -> impl Strategy<Value = Value> {
    match type_id {
        CqlTypeId::Int => any::<i32>().prop_map(Value::Integer).boxed(),
        CqlTypeId::BigInt => any::<i64>().prop_map(Value::BigInt).boxed(),
        CqlTypeId::Boolean => any::<bool>().prop_map(Value::Boolean).boxed(),
        CqlTypeId::Varchar => any::<String>()
            .prop_filter("non-empty strings", |s| !s.is_empty())
            .prop_map(Value::Text)
            .boxed(),
        _ => any::<i32>().prop_map(Value::Integer).boxed(),
    }
}

/// Generate a list with potential null elements
fn arb_list_with_nulls(max_size: usize) -> impl Strategy<Value = Value> {
    prop::collection::vec(
        prop_oneof![
            3 => arb_simple_value(CqlTypeId::Int),
            1 => Just(Value::Null)
        ],
        0..max_size,
    )
    .prop_map(Value::List)
}

/// Generate a map with potential null keys/values
fn arb_map_with_nulls(max_size: usize) -> impl Strategy<Value = Value> {
    prop::collection::vec(
        (
            prop_oneof![
                3 => arb_simple_value(CqlTypeId::Int),
                1 => Just(Value::Null)
            ],
            prop_oneof![
                3 => arb_simple_value(CqlTypeId::Int),
                1 => Just(Value::Null)
            ],
        ),
        0..max_size,
    )
    .prop_map(Value::Map)
}

// ============================================================================
// Property Tests
// ============================================================================

proptest! {
    /// Test that list parsing with schema produces deterministic results
    #[test]
    fn prop_list_roundtrip_deterministic(list in arb_list_with_nulls(10)) {
        // Serialize the list
        let mut serialized = Vec::new();

        // Count non-null elements
        let elements = if let Value::List(ref els) = list {
            els
        } else {
            panic!("Expected list");
        };

        serialized.extend(encode_vint(elements.len() as i64));

        for element in elements {
            match element {
                Value::Null => {
                    serialized.extend(encode_vint(-1));
                }
                Value::Integer(i) => {
                    serialized.extend(encode_vint(4));
                    serialized.extend_from_slice(&i.to_be_bytes());
                }
                _ => {}
            }
        }

        // Parse with schema
        let schema = CqlType::Int;
        let result = parse_list_with_schema(&serialized, &schema);

        // Should succeed
        prop_assert!(result.is_ok());
        let (remaining, parsed) = result.unwrap();

        // Should consume all bytes
        prop_assert_eq!(remaining.len(), 0, "Buffer not fully consumed");

        // Should match original structure
        prop_assert_eq!(
            if let Value::List(els) = &parsed { els.len() } else { 0 },
            elements.len()
        );
    }

    /// Test that map parsing with schema handles null keys and values correctly
    #[test]
    fn prop_map_roundtrip_with_nulls(map in arb_map_with_nulls(5)) {
        // Serialize the map
        let mut serialized = Vec::new();

        let pairs = if let Value::Map(ref ps) = map {
            ps
        } else {
            panic!("Expected map");
        };

        serialized.extend(encode_vint(pairs.len() as i64));

        for (key, value) in pairs {
            // Serialize key
            match key {
                Value::Null => {
                    serialized.extend(encode_vint(-1));
                }
                Value::Integer(i) => {
                    serialized.extend(encode_vint(4));
                    serialized.extend_from_slice(&i.to_be_bytes());
                }
                _ => {}
            }

            // Serialize value
            match value {
                Value::Null => {
                    serialized.extend(encode_vint(-1));
                }
                Value::Integer(i) => {
                    serialized.extend(encode_vint(4));
                    serialized.extend_from_slice(&i.to_be_bytes());
                }
                _ => {}
            }
        }

        // Parse with schema
        let key_schema = CqlType::Int;
        let value_schema = CqlType::Int;
        let result = parse_map_with_schema(&serialized, &key_schema, &value_schema);

        // Should succeed
        prop_assert!(result.is_ok());
        let (remaining, parsed) = result.unwrap();

        // Should consume all bytes
        prop_assert_eq!(remaining.len(), 0, "Buffer not fully consumed in map");

        // Should match original structure
        prop_assert_eq!(
            if let Value::Map(ps) = &parsed { ps.len() } else { 0 },
            pairs.len()
        );
    }

    /// Test that nested collections are handled correctly
    #[test]
    fn prop_nested_list_of_lists(
        outer_size in 0usize..5,
        inner_size in 0usize..3,
        use_nulls in any::<bool>()
    ) {
        // Create a list of lists
        let mut inner_lists = Vec::new();
        for _ in 0..outer_size {
            if use_nulls && inner_lists.len() % 2 == 0 {
                inner_lists.push(Value::Null);
            } else {
                let mut inner = Vec::new();
                for j in 0..inner_size {
                    inner.push(Value::Integer(j as i32));
                }
                inner_lists.push(Value::List(inner));
            }
        }

        let nested_list = Value::List(inner_lists);

        // For this test, we verify the structure is valid
        // (Full serialization/deserialization would require recursive schema handling)
        if let Value::List(outer) = &nested_list {
            prop_assert_eq!(outer.len(), outer_size);

            for elem in outer {
                match elem {
                    Value::Null => {
                        // Null is valid
                    }
                    Value::List(inner) => {
                        prop_assert!(inner.len() <= inner_size);
                    }
                    _ => prop_assert!(false, "Unexpected element type"),
                }
            }
        }
    }
}

// ============================================================================
// Unit Tests for Edge Cases
// ============================================================================

#[test]
fn test_empty_list_with_schema() {
    let mut data = Vec::new();
    data.extend(encode_vint(0)); // zero elements

    let schema = CqlType::Int;
    let result = parse_list_with_schema(&data, &schema);

    assert!(result.is_ok());
    let (remaining, parsed) = result.unwrap();
    assert_eq!(remaining.len(), 0, "Buffer should be fully consumed");
    assert_eq!(parsed, Value::List(Vec::new()));
}

#[test]
fn test_list_all_nulls_with_schema() {
    let mut data = Vec::new();
    data.extend(encode_vint(3)); // three elements

    // Three null elements
    data.extend(encode_vint(-1));
    data.extend(encode_vint(-1));
    data.extend(encode_vint(-1));

    let schema = CqlType::Int;
    let result = parse_list_with_schema(&data, &schema);

    assert!(result.is_ok());
    let (remaining, parsed) = result.unwrap();
    assert_eq!(remaining.len(), 0, "Buffer should be fully consumed");
    assert_eq!(
        parsed,
        Value::List(vec![Value::Null, Value::Null, Value::Null])
    );
}

#[test]
fn test_map_empty_with_schema() {
    let mut data = Vec::new();
    data.extend(encode_vint(0)); // zero pairs

    let key_schema = CqlType::Int;
    let value_schema = CqlType::Text;
    let result = parse_map_with_schema(&data, &key_schema, &value_schema);

    assert!(result.is_ok());
    let (remaining, parsed) = result.unwrap();
    assert_eq!(remaining.len(), 0, "Buffer should be fully consumed");
    assert_eq!(parsed, Value::Map(Vec::new()));
}

#[test]
fn test_map_with_null_value() {
    let mut data = Vec::new();
    data.extend(encode_vint(1)); // one pair

    // Key: integer 42
    data.extend(encode_vint(4));
    data.extend_from_slice(&42i32.to_be_bytes());

    // Value: null
    data.extend(encode_vint(-1));

    let key_schema = CqlType::Int;
    let value_schema = CqlType::Text;
    let result = parse_map_with_schema(&data, &key_schema, &value_schema);

    assert!(result.is_ok());
    let (remaining, parsed) = result.unwrap();
    assert_eq!(remaining.len(), 0, "Buffer should be fully consumed");

    if let Value::Map(pairs) = parsed {
        assert_eq!(pairs.len(), 1);
        assert_eq!(pairs[0].0, Value::Integer(42));
        assert_eq!(pairs[0].1, Value::Null);
    } else {
        panic!("Expected map value");
    }
}

#[test]
fn test_buffer_not_fully_consumed_detected() {
    let mut data = Vec::new();
    data.extend(encode_vint(1)); // one element

    // Element: integer 42
    data.extend(encode_vint(4));
    data.extend_from_slice(&42i32.to_be_bytes());

    // Add extra garbage bytes
    data.extend_from_slice(&[0xFF, 0xFF]);

    let schema = CqlType::Int;
    let result = parse_list_with_schema(&data, &schema);

    // Should succeed but leave garbage bytes
    assert!(result.is_ok());
    let (remaining, _) = result.unwrap();
    assert_eq!(remaining.len(), 2, "Extra bytes should remain");
}