use ftui_render::alloc_budget::{AllocLeakDetector, LeakDetectorConfig};
use ftui_render::buffer::AdaptiveDoubleBuffer;
#[test]
fn resize_storm_achieves_high_avoidance() {
let mut adb = AdaptiveDoubleBuffer::new(80, 24);
for i in 0..100 {
let delta_w = ((i % 20) as i16 - 10).unsigned_abs();
let delta_h = ((i % 10) as i16 - 5).unsigned_abs();
adb.resize(80 + delta_w, 24 + delta_h);
}
let stats = adb.stats();
let ratio = stats.avoidance_ratio();
assert!(
ratio >= 0.80,
"Resize storm should achieve >= 80% avoidance, got {:.2}%",
ratio * 100.0
);
}
#[test]
fn gradual_growth_reuses_capacity() {
let mut adb = AdaptiveDoubleBuffer::new(80, 24);
for i in 1..=15 {
adb.resize(80 + i, 24);
}
let stats = adb.stats();
assert!(
stats.resize_avoided >= 10,
"Expected >= 10 avoided resizes, got {}",
stats.resize_avoided
);
}
#[test]
fn shrink_grow_cycle_no_thrash() {
let mut adb = AdaptiveDoubleBuffer::new(100, 40);
for _ in 0..10 {
adb.resize(80, 30); adb.resize(100, 40); }
let stats = adb.stats();
let ratio = stats.avoidance_ratio();
assert!(
ratio >= 0.9,
"Shrink-grow cycle should achieve >= 90% avoidance, got {:.2}%",
ratio * 100.0
);
}
#[test]
fn memory_overhead_within_budget() {
let test_sizes = [(80, 24), (120, 40), (200, 60), (1000, 500)];
for (w, h) in test_sizes {
let adb = AdaptiveDoubleBuffer::new(w, h);
let efficiency = adb.memory_efficiency();
assert!(
efficiency > 0.50,
"Memory efficiency for {}x{} should be > 50%, got {:.1}%",
w,
h,
efficiency * 100.0
);
}
}
#[test]
fn small_buffer_reasonable_overhead() {
let adb = AdaptiveDoubleBuffer::new(10, 5);
let efficiency = adb.memory_efficiency();
assert!(
efficiency > 0.40,
"Small buffer efficiency should be > 40%, got {:.1}%",
efficiency * 100.0
);
}
#[test]
fn large_buffer_capped_overage() {
let adb = AdaptiveDoubleBuffer::new(2000, 1000);
assert_eq!(adb.capacity_width(), 2200);
assert_eq!(adb.capacity_height(), 1200);
let efficiency = adb.memory_efficiency();
assert!(
efficiency > 0.75,
"Large buffer efficiency should be > 75%, got {:.1}%",
efficiency * 100.0
);
}
#[test]
fn stable_operations_no_leak_alert() {
let config = LeakDetectorConfig {
warmup_frames: 20,
..LeakDetectorConfig::default()
};
let mut detector = AllocLeakDetector::new(config);
let mut adb = AdaptiveDoubleBuffer::new(80, 24);
let mut prev_realloc = 0u64;
for frame in 0..100 {
let w = 80 + (frame % 5) as u16;
let h = 24 + (frame % 3) as u16;
adb.resize(w, h);
let current_realloc = adb.stats().resize_reallocated;
let did_realloc = current_realloc > prev_realloc;
prev_realloc = current_realloc;
let alloc_proxy = if did_realloc {
200.0 } else {
100.0 + (frame % 10) as f64 };
let alert = detector.observe(alloc_proxy);
assert!(
!alert.triggered,
"Stable resize pattern should not trigger leak alert at frame {}",
frame
);
}
}
#[test]
fn detector_catches_allocation_regression() {
let config = LeakDetectorConfig {
warmup_frames: 20,
lambda: 0.3,
..LeakDetectorConfig::default()
};
let mut detector = AllocLeakDetector::new(config);
for _ in 0..30 {
detector.observe(100.0);
}
let mut detected = false;
for i in 0..100 {
let alert = detector.observe(150.0);
if alert.triggered {
detected = true;
assert!(
i < 50,
"Should detect regression within 50 frames, took {}",
i
);
break;
}
}
assert!(detected, "Should detect allocation regression");
}
#[test]
fn e2e_reflow_budget_verification() {
let config = LeakDetectorConfig {
warmup_frames: 20,
..LeakDetectorConfig::default()
};
let mut detector = AllocLeakDetector::new(config);
let mut adb = AdaptiveDoubleBuffer::new(80, 24);
let mut total_resizes = 0u64;
let mut realloc_events = 0u64;
let mut prev_realloc = 0u64;
for frame in 0..200 {
let phase = frame / 50;
let (w, h) = match phase {
0 => (80 + (frame % 20) as u16, 24 + (frame % 10) as u16 / 2),
1 => (100 - (frame % 15) as u16, 29 - (frame % 5) as u16),
2 => (85 + (frame % 25) as u16, 24 + (frame % 15) as u16),
_ => (80, 24),
};
let old_realloc = adb.stats().resize_reallocated;
if adb.resize(w, h) {
total_resizes += 1;
if adb.stats().resize_reallocated > old_realloc {
realloc_events += 1;
}
}
let current_realloc = adb.stats().resize_reallocated;
let did_realloc = current_realloc > prev_realloc;
prev_realloc = current_realloc;
let alloc_proxy = if did_realloc {
200.0 } else {
100.0 + (frame % 15) as f64 };
detector.observe(alloc_proxy);
}
let stats = adb.stats();
let avoidance = stats.avoidance_ratio();
let efficiency = adb.memory_efficiency();
assert!(
avoidance >= 0.70,
"E2E avoidance ratio should be >= 70%, got {:.1}%",
avoidance * 100.0
);
assert!(
efficiency >= 0.35,
"E2E memory efficiency should be >= 35%, got {:.1}%",
efficiency * 100.0
);
let summary = format!(
r#"{{"test":"e2e_reflow_budget","total_resizes":{},"realloc_events":{},"avoidance_ratio":{:.4},"memory_efficiency":{:.4},"final_e_value":{:.4}}}"#,
total_resizes,
realloc_events,
avoidance,
efficiency,
detector.e_value(),
);
assert!(summary.starts_with('{') && summary.ends_with('}'));
assert!(summary.contains("\"avoidance_ratio\":"));
}
#[test]
fn property_resize_never_panics() {
let mut adb = AdaptiveDoubleBuffer::new(80, 24);
let test_sizes = [
(1, 1),
(10, 5),
(80, 24),
(120, 40),
(200, 60),
(500, 200),
(1000, 500),
(u16::MAX / 2, u16::MAX / 2),
];
for (w, h) in test_sizes {
adb.resize(w, h);
assert_eq!(adb.width(), w);
assert_eq!(adb.height(), h);
}
}
#[test]
fn property_capacity_invariant() {
let mut adb = AdaptiveDoubleBuffer::new(80, 24);
let sizes = [
(100, 50),
(50, 25),
(150, 70),
(30, 15),
(200, 100),
(80, 24),
(120, 40),
(60, 30),
(180, 90),
(40, 20),
];
for (w, h) in sizes {
adb.resize(w, h);
assert!(adb.capacity_width() >= adb.width());
assert!(adb.capacity_height() >= adb.height());
}
}
#[test]
fn property_stats_consistent() {
let mut adb = AdaptiveDoubleBuffer::new(80, 24);
let mut total_resizes = 0u64;
let sizes = [
(90, 28),
(100, 35),
(150, 50),
(50, 20),
(80, 24),
(85, 26),
(200, 80),
(60, 25),
];
for (w, h) in sizes {
if adb.resize(w, h) {
total_resizes += 1;
}
}
let stats = adb.stats();
let total_from_stats = stats.resize_avoided + stats.resize_reallocated;
assert_eq!(
total_from_stats, total_resizes,
"Stats should be consistent: avoided({}) + reallocated({}) = {}",
stats.resize_avoided, stats.resize_reallocated, total_resizes
);
}