pmcp 2.1.0

High-quality Rust SDK for Model Context Protocol (MCP) with full TypeScript SDK compatibility
Documentation
//! Property-based tests for connection pooling and load balancing
//!
//! PMCP-4003: Property tests for pool management and load distribution

use pmcp::shared::{ConnectionPoolConfig, HealthStatus, LoadBalanceStrategy, PoolStats};
use proptest::prelude::*;
use std::time::Duration;

#[cfg(test)]
mod pool_configuration {
    use super::*;

    proptest! {
        /// Property: Pool size constraints are respected
        #[test]
        fn property_pool_size_constraints(
            max_connections in 1usize..100,
            min_connections in 1usize..50
        ) {
            let min_connections = std::cmp::min(min_connections, max_connections);

            let config = ConnectionPoolConfig {
                max_connections,
                min_connections,
                ..Default::default()
            };

            prop_assert!(config.min_connections <= config.max_connections);
            prop_assert!(config.min_connections > 0);
            prop_assert_eq!(config.min_connections, min_connections);
            prop_assert_eq!(config.max_connections, max_connections);
        }

        /// Property: Load balancing strategies are configurable
        #[test]
        fn property_load_balance_strategies(
            strategy in prop::sample::select(&[
                LoadBalanceStrategy::RoundRobin,
                LoadBalanceStrategy::LeastConnections,
                LoadBalanceStrategy::WeightedRoundRobin,
                LoadBalanceStrategy::Random,
            ])
        ) {
            let config = ConnectionPoolConfig {
                strategy,
                ..Default::default()
            };

            prop_assert_eq!(config.strategy, strategy);
        }

        /// Property: Health check intervals are configurable
        #[test]
        fn property_health_check_intervals(
            interval_secs in 1u64..300
        ) {
            let config = ConnectionPoolConfig {
                health_check_interval: Duration::from_secs(interval_secs),
                ..Default::default()
            };

            prop_assert_eq!(config.health_check_interval.as_secs(), interval_secs);
            prop_assert!(config.health_check_interval > Duration::ZERO);
        }

        /// Property: Operation timeouts are configurable
        #[test]
        fn property_operation_timeouts(
            timeout_secs in 1u64..60
        ) {
            let config = ConnectionPoolConfig {
                operation_timeout: Duration::from_secs(timeout_secs),
                ..Default::default()
            };

            prop_assert_eq!(config.operation_timeout.as_secs(), timeout_secs);
            prop_assert!(config.operation_timeout > Duration::ZERO);
        }

        /// Property: Auto-scaling configuration
        #[test]
        fn property_auto_scaling_config(
            auto_scaling in any::<bool>()
        ) {
            let config = ConnectionPoolConfig {
                auto_scaling,
                ..Default::default()
            };

            prop_assert_eq!(config.auto_scaling, auto_scaling);
        }

        /// Property: Retry configuration
        #[test]
        fn property_retry_configuration(
            max_retries in 0usize..10,
            retry_delay_secs in 1u64..30
        ) {
            let config = ConnectionPoolConfig {
                max_retries,
                retry_delay: Duration::from_secs(retry_delay_secs),
                ..Default::default()
            };

            prop_assert_eq!(config.max_retries, max_retries);
            prop_assert_eq!(config.retry_delay.as_secs(), retry_delay_secs);
        }
    }
}

#[cfg(test)]
mod health_status {
    use super::*;

    proptest! {
        /// Property: Health status transitions are logical
        #[test]
        fn property_health_status_transitions(
            initial_status in prop::sample::select(&[
                HealthStatus::Healthy,
                HealthStatus::Degraded,
                HealthStatus::Unhealthy,
                HealthStatus::Checking,
            ])
        ) {
            // Health status should be one of the valid enum values
            match initial_status {
                HealthStatus::Healthy => prop_assert!(true),
                HealthStatus::Degraded => prop_assert!(true),
                HealthStatus::Unhealthy => prop_assert!(true),
                HealthStatus::Checking => prop_assert!(true),
            }

            // Status equality should work
            prop_assert_eq!(initial_status, initial_status);
        }

        /// Property: Health status ordering makes sense
        #[test]
        fn property_health_status_priority(
            status in prop::sample::select(&[
                HealthStatus::Healthy,
                HealthStatus::Degraded,
                HealthStatus::Unhealthy,
                HealthStatus::Checking,
            ])
        ) {
            // Healthy connections should be preferred
            let is_usable = matches!(status,
                HealthStatus::Healthy | HealthStatus::Degraded
            );

            match status {
                HealthStatus::Healthy => prop_assert!(is_usable),
                HealthStatus::Degraded => prop_assert!(is_usable),
                HealthStatus::Unhealthy => prop_assert!(!is_usable),
                HealthStatus::Checking => prop_assert!(!is_usable),
            }
        }
    }
}

#[cfg(test)]
mod pool_stats {
    use super::*;

    proptest! {
        /// Property: Pool statistics are consistent
        #[test]
        fn property_pool_stats_consistency(
            healthy in 0usize..50,
            degraded in 0usize..30,
            unhealthy in 0usize..20,
            total_requests in 0u64..10000,
            active_requests_percentage in 0u8..=100
        ) {
            let total_connections = healthy + degraded + unhealthy;

            // Ensure active_requests doesn't exceed total_requests or connection limits
            let max_active_by_requests = total_requests as usize;
            let max_active_by_connections = if total_connections > 0 { total_connections * 100 } else { 0 };
            let max_active = max_active_by_requests.min(max_active_by_connections);
            let active_requests = (max_active * active_requests_percentage as usize) / 100;

            let stats = PoolStats {
                total_connections,
                healthy_connections: healthy,
                degraded_connections: degraded,
                unhealthy_connections: unhealthy,
                total_requests,
                active_requests,
                strategy: LoadBalanceStrategy::RoundRobin,
            };

            // Total connections should equal sum of health statuses
            prop_assert_eq!(
                stats.total_connections,
                stats.healthy_connections + stats.degraded_connections + stats.unhealthy_connections
            );

            // Active requests should not exceed some reasonable bound relative to connections
            if total_connections > 0 {
                prop_assert!(stats.active_requests <= total_connections * 100); // Max 100 requests per connection
            }

            // Total requests should be >= active requests
            prop_assert!(stats.total_requests >= stats.active_requests as u64);
        }

        /// Property: Pool utilization calculations
        #[test]
        fn property_pool_utilization(
            total_connections in 1usize..100,
            active_requests in 0usize..1000
        ) {
            // Calculate utilization metrics
            let avg_requests_per_connection = if total_connections > 0 {
                active_requests as f64 / total_connections as f64
            } else {
                0.0
            };

            prop_assert!(avg_requests_per_connection >= 0.0);
            prop_assert!(avg_requests_per_connection <= active_requests as f64);
        }
    }
}

#[cfg(test)]
mod load_balancing {
    use super::*;

    proptest! {
        /// Property: Load balancing strategies produce valid selections
        #[test]
        fn property_load_balancing_selection(
            connection_count in 1usize..20,
            strategy in prop::sample::select(&[
                LoadBalanceStrategy::RoundRobin,
                LoadBalanceStrategy::LeastConnections,
                LoadBalanceStrategy::WeightedRoundRobin,
                LoadBalanceStrategy::Random,
            ])
        ) {
            // Simulate connection selection logic
            let connections: Vec<usize> = (0..connection_count).collect();

            // For round-robin, selection should cycle through all connections
            if strategy == LoadBalanceStrategy::RoundRobin {
                for i in 0..connection_count * 2 {
                    let selected_index = i % connection_count;
                    prop_assert!(selected_index < connection_count);
                    prop_assert!(connections.contains(&selected_index));
                }
            }

            // All strategies should produce valid connection indices
            prop_assert!(connection_count > 0);
            prop_assert_eq!(connections.len(), connection_count);
        }

        /// Property: Round-robin distribution is even over time
        #[test]
        fn property_round_robin_distribution(
            connection_count in 2usize..10,
            request_count in 10usize..100
        ) {
            let mut selection_counts = vec![0usize; connection_count];

            // Simulate round-robin selections
            for i in 0..request_count {
                let selected = i % connection_count;
                selection_counts[selected] += 1;
            }

            // Check distribution is relatively even
            let expected_per_connection = request_count / connection_count;
            let remainder = request_count % connection_count;

            for (i, &count) in selection_counts.iter().enumerate() {
                if i < remainder {
                    prop_assert_eq!(count, expected_per_connection + 1);
                } else {
                    prop_assert_eq!(count, expected_per_connection);
                }
            }
        }

        /// Property: Least connections strategy prefers underutilized connections
        #[test]
        fn property_least_connections_preference(
            active_requests in prop::collection::vec(0usize..50, 2..10)
        ) {
            let min_requests = *active_requests.iter().min().unwrap();
            let min_indices: Vec<usize> = active_requests
                .iter()
                .enumerate()
                .filter(|(_, &requests)| requests == min_requests)
                .map(|(i, _)| i)
                .collect();

            // Any connection with minimum active requests should be valid choice
            prop_assert!(!min_indices.is_empty());
            prop_assert!(min_indices.iter().all(|&i| i < active_requests.len()));

            // All connections with minimum requests have the same count
            for &index in &min_indices {
                prop_assert_eq!(active_requests[index], min_requests);
            }
        }
    }
}