use limiteron::MockClock;
use limiteron::limiters::{FixedWindowLimiter, Limiter};
use proptest::prelude::*;
use std::sync::Arc;
use std::time::Duration;
proptest! {
#![proptest_config(ProptestConfig::with_cases(100))]
#[test]
fn test_fixed_window_respects_limit(
max_requests in 1u64..500,
window_seconds in 1u64..60,
num_requests in 1usize..2000
) {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn limiteron::Clock> = mock_clock.clone();
let limiter = FixedWindowLimiter::with_clock(
Duration::from_secs(window_seconds),
max_requests,
clock,
);
let rt = tokio::runtime::Runtime::new().unwrap();
let mut allowed_count = 0u64;
for _ in 0..num_requests {
let allowed = rt.block_on(limiter.allow(1)).unwrap();
if allowed {
allowed_count += 1;
}
}
prop_assert!(
allowed_count <= max_requests,
"Allowed {} requests in window but limit is {}",
allowed_count,
max_requests
);
}
#[test]
fn test_fixed_window_resets_correctly(
max_requests in 1u64..100,
window_seconds in 1u64..30,
num_windows in 1usize..5
) {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn limiteron::Clock> = mock_clock.clone();
let limiter = FixedWindowLimiter::with_clock(
Duration::from_secs(window_seconds),
max_requests,
clock,
);
let rt = tokio::runtime::Runtime::new().unwrap();
let mut total_allowed = 0u64;
for _window in 0..num_windows {
let mut window_allowed = 0u64;
for _ in 0..(max_requests + 10) {
let allowed = rt.block_on(limiter.allow(1)).unwrap();
if allowed {
window_allowed += 1;
} else {
break;
}
}
prop_assert_eq!(
window_allowed,
max_requests,
"Window allowed {} but expected {}",
window_allowed,
max_requests
);
total_allowed += window_allowed;
mock_clock.advance(Duration::from_secs(window_seconds + 1));
}
prop_assert_eq!(
total_allowed,
(num_windows as u64) * max_requests,
"Total allowed {} but expected {} ({} windows * {} limit)",
total_allowed,
(num_windows as u64) * max_requests,
num_windows,
max_requests
);
}
#[test]
fn test_fixed_window_boundary_behavior(
max_requests in 1u64..50,
window_seconds in 1u64..10
) {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn limiteron::Clock> = mock_clock.clone();
let limiter = FixedWindowLimiter::with_clock(
Duration::from_secs(window_seconds),
max_requests,
clock,
);
let rt = tokio::runtime::Runtime::new().unwrap();
for _ in 0..max_requests {
let _ = rt.block_on(limiter.allow(1));
}
prop_assert!(!rt.block_on(limiter.allow(1)).unwrap());
mock_clock.advance(Duration::from_secs(window_seconds + 1));
prop_assert!(rt.block_on(limiter.allow(1)).unwrap());
}
#[test]
fn test_fixed_window_large_cost_requests(
max_requests in 10u64..100,
window_seconds in 1u64..60,
cost in 1u64..50
) {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn limiteron::Clock> = mock_clock.clone();
let limiter = FixedWindowLimiter::with_clock(
Duration::from_secs(window_seconds),
max_requests,
clock,
);
let rt = tokio::runtime::Runtime::new().unwrap();
let mut total_cost = 0u64;
let num_requests = 100;
for _ in 0..num_requests {
let allowed = rt.block_on(limiter.allow(cost)).unwrap();
if allowed {
total_cost += cost;
}
}
prop_assert!(
total_cost <= max_requests,
"Total cost {} exceeded max_requests {}",
total_cost,
max_requests
);
}
#[test]
fn test_fixed_window_cumulative_count_across_windows(
max_requests in 1u64..20,
window_seconds in 1u64..10
) {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn limiteron::Clock> = mock_clock.clone();
let limiter = FixedWindowLimiter::with_clock(
Duration::from_secs(window_seconds),
max_requests,
clock,
);
let rt = tokio::runtime::Runtime::new().unwrap();
for window in 0..3 {
let mut count = 0u64;
for _ in 0..(max_requests * 2) {
if rt.block_on(limiter.allow(1)).unwrap() {
count += 1;
}
}
prop_assert_eq!(
count,
max_requests,
"Window {} had count {} but expected {}",
window,
count,
max_requests
);
mock_clock.advance(Duration::from_secs(window_seconds + 1));
}
}
}
#[cfg(test)]
mod additional_tests {
use super::*;
#[tokio::test]
async fn test_fixed_window_immediate_reset() {
let mock_clock = Arc::new(MockClock::new());
let clock: Arc<dyn limiteron::Clock> = mock_clock.clone();
let limiter = FixedWindowLimiter::with_clock(Duration::from_secs(1), 5, clock);
for _ in 0..5 {
assert!(limiter.allow(1).await.unwrap());
}
assert!(!limiter.allow(1).await.unwrap());
mock_clock.advance(Duration::from_secs(2));
assert!(limiter.allow(1).await.unwrap());
}
}