use super::*;
#[test]
fn shrink_under_over_target() {
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(2 * target);
let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 64 * 1024 * 1024);
let b = FakeCache::new("b", 32 * 1024 * 1024);
a.set_current_bytes(64 * 1024 * 1024);
b.set_current_bytes(32 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
let dyn_b: Arc<dyn Resizable> = b.clone();
budget.register(&dyn_a);
budget.register(&dyn_b);
let snap = budget.tick_now();
assert!(snap.caches.iter().any(|c| c.name == "a"));
assert_eq!(a.max_bytes(), 32 * 1024 * 1024);
assert_eq!(b.max_bytes(), 16 * 1024 * 1024);
}
#[test]
fn hold_inside_hysteresis_band() {
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(target);
let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 64 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let before = a.max_bytes();
budget.tick_now();
assert_eq!(a.max_bytes(), before, "caps must hold in band");
}
#[test]
fn grow_under_target() {
let target = 200 * 1024 * 1024;
let rss = FakeRss::new(target / 2);
let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 32 * 1024 * 1024);
a.set_hits(900, 100); let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
budget.tick_now();
assert_eq!(a.max_bytes(), 32 * 1024 * 1024);
}
#[test]
fn lease_reduces_effective_target() {
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(target); let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 80 * 1024 * 1024);
a.set_current_bytes(80 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
budget.tick_now();
assert_eq!(a.max_bytes(), 80 * 1024 * 1024);
let lease = budget.lease(50 * 1024 * 1024);
assert_eq!(budget.effective_target_bytes(), 50 * 1024 * 1024);
budget.tick_now();
assert_eq!(a.max_bytes(), 40 * 1024 * 1024);
drop(lease);
assert_eq!(budget.effective_target_bytes(), target);
}
#[test]
fn executor_spill_run_buffer_uses_effective_budget_slice() {
let target = 64 * 1024 * 1024;
let rss = FakeRss::new(target);
let budget = budget_with_target(target, Arc::clone(&rss));
assert_eq!(budget.executor_spill_run_buffer_bytes(), 4 * 1024 * 1024);
let lease = budget.lease(32 * 1024 * 1024);
assert_eq!(budget.executor_spill_run_buffer_bytes(), 2 * 1024 * 1024);
drop(lease);
assert_eq!(budget.executor_spill_run_buffer_bytes(), 4 * 1024 * 1024);
}
#[test]
fn skip_shrink_when_caches_lean_vs_rss() {
use crate::policy::Verdict;
let target = 100 * 1024 * 1024;
let rss_value = 1024 * 1024 * 1024_u64; let rss = FakeRss::new(rss_value);
let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 80 * 1024 * 1024);
a.set_current_bytes(50 * 1024 * 1024); let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let before_cap = a.max_bytes();
let snap = budget.tick_now();
assert_eq!(
a.max_bytes(),
before_cap,
"lean-cache shrink should hold caps, got {}",
a.max_bytes()
);
match &snap.verdict {
Verdict::SkipShrinkLeanCaches {
cache_share,
cache_bytes,
rss_bytes,
} => {
assert!(
*cache_share < 0.25,
"expected lean cache share, got {cache_share}"
);
assert_eq!(*cache_bytes, 50 * 1024 * 1024);
assert_eq!(*rss_bytes, rss_value);
}
other => panic!("expected SkipShrinkLeanCaches, got {other:?}"),
}
}
#[test]
fn shrink_still_fires_when_caches_dominate_rss() {
use crate::policy::Verdict;
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(2 * target);
let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 64 * 1024 * 1024);
a.set_current_bytes(64 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let snap = budget.tick_now();
match snap.verdict {
Verdict::Shrink { factor } => {
assert!(
(factor - 0.5).abs() < 1e-6,
"expected factor ≈ 0.5, got {factor}"
);
}
other => panic!("expected Shrink, got {other:?}"),
}
}
#[test]
fn skip_shrink_when_rss_grew_but_caches_flat() {
use crate::policy::Verdict;
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(90 * 1024 * 1024); let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 80 * 1024 * 1024);
a.set_current_bytes(60 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let _ = budget.tick_now();
rss.rss.store(200 * 1024 * 1024, Ordering::Relaxed);
let before_cap = a.max_bytes();
let snap = budget.tick_now();
assert_eq!(
a.max_bytes(),
before_cap,
"flat-cache shrink should hold caps, got {}",
a.max_bytes()
);
match &snap.verdict {
Verdict::SkipShrinkFlatCaches {
cache_bytes,
previous_cache_bytes,
cache_count,
previous_cache_count,
rss_bytes,
} => {
assert_eq!(*cache_bytes, 60 * 1024 * 1024);
assert_eq!(*previous_cache_bytes, 60 * 1024 * 1024);
assert_eq!(*cache_count, 1);
assert_eq!(*previous_cache_count, 1);
assert_eq!(*rss_bytes, 200 * 1024 * 1024);
}
other => panic!("expected SkipShrinkFlatCaches, got {other:?}"),
}
}
#[test]
fn shrink_still_fires_when_caches_grew_alongside_rss() {
use crate::policy::Verdict;
let target = 100 * 1024 * 1024;
let rss = FakeRss::new(90 * 1024 * 1024); let budget = budget_with_target(target, Arc::clone(&rss));
let a = FakeCache::new("a", 80 * 1024 * 1024);
a.set_current_bytes(60 * 1024 * 1024);
let dyn_a: Arc<dyn Resizable> = a.clone();
budget.register(&dyn_a);
let _ = budget.tick_now();
rss.rss.store(200 * 1024 * 1024, Ordering::Relaxed);
a.set_current_bytes(80 * 1024 * 1024);
let snap = budget.tick_now();
match snap.verdict {
Verdict::Shrink { factor } => {
assert!(
(factor - 0.5).abs() < 1e-6,
"expected factor ≈ 0.5, got {factor}"
);
}
other => panic!("expected Shrink, got {other:?}"),
}
}