#![forbid(unsafe_code)]
use ftui_core::event::Event;
use ftui_harness::input_storm::{
BurstPattern, InputStormConfig, generate_storm, run_storm_with_logging,
};
use ftui_render::frame::Frame;
use ftui_runtime::program::{Cmd, Model};
use ftui_runtime::simulator::ProgramSimulator;
struct StormTestModel {
key_count: usize,
mouse_count: usize,
paste_count: usize,
paste_bytes: usize,
resize_count: usize,
last_resize: Option<(u16, u16)>,
total_events: usize,
}
impl StormTestModel {
fn new() -> Self {
Self {
key_count: 0,
mouse_count: 0,
paste_count: 0,
paste_bytes: 0,
resize_count: 0,
last_resize: None,
total_events: 0,
}
}
}
#[derive(Debug)]
enum StormMsg {
KeyPressed,
MouseMoved,
Pasted(usize),
Resized(u16, u16),
Other,
}
impl From<Event> for StormMsg {
fn from(event: Event) -> Self {
match event {
Event::Key(_) => StormMsg::KeyPressed,
Event::Mouse(_) => StormMsg::MouseMoved,
Event::Paste(pe) => StormMsg::Pasted(pe.text.len()),
Event::Resize { width, height } => StormMsg::Resized(width, height),
_ => StormMsg::Other,
}
}
}
impl Model for StormTestModel {
type Message = StormMsg;
fn update(&mut self, msg: Self::Message) -> Cmd<Self::Message> {
self.total_events += 1;
match msg {
StormMsg::KeyPressed => self.key_count += 1,
StormMsg::MouseMoved => self.mouse_count += 1,
StormMsg::Pasted(bytes) => {
self.paste_count += 1;
self.paste_bytes += bytes;
}
StormMsg::Resized(w, h) => {
self.resize_count += 1;
self.last_resize = Some((w, h));
}
StormMsg::Other => {}
}
Cmd::None
}
fn view(&self, _frame: &mut Frame) {
}
}
fn run_storm_in_simulator(storm_events: &[Event]) -> StormTestModel {
let mut sim = ProgramSimulator::new(StormTestModel::new());
sim.init();
sim.inject_events(storm_events);
let model = sim.model();
StormTestModel {
key_count: model.key_count,
mouse_count: model.mouse_count,
paste_count: model.paste_count,
paste_bytes: model.paste_bytes,
resize_count: model.resize_count,
last_resize: model.last_resize,
total_events: model.total_events,
}
}
#[test]
fn e2e_keyboard_storm_1000_events() {
let config = InputStormConfig::new(BurstPattern::KeyboardStorm { count: 1000 }, 42);
let storm = generate_storm(&config);
let (processed, log_lines) = run_storm_with_logging(&storm);
assert_eq!(processed, 1000);
let model = run_storm_in_simulator(&storm.events);
assert_eq!(
model.key_count, 1000,
"All keyboard events must be processed"
);
assert_eq!(model.total_events, 1000);
for line in &log_lines {
let val: serde_json::Value = serde_json::from_str(line).unwrap();
assert!(val["event"].is_string());
}
eprintln!(
"[keyboard_storm] {} events, {} JSONL lines",
model.key_count,
log_lines.len()
);
}
#[test]
fn e2e_mouse_flood_1000_events() {
let config = InputStormConfig::new(
BurstPattern::MouseFlood {
count: 1000,
width: 80,
height: 24,
},
42,
);
let storm = generate_storm(&config);
let model = run_storm_in_simulator(&storm.events);
assert_eq!(
model.mouse_count, 1000,
"All mouse events must be processed"
);
assert_eq!(model.total_events, 1000);
eprintln!("[mouse_flood] {} mouse events processed", model.mouse_count);
}
#[test]
fn e2e_mixed_burst_1000_events() {
let config = InputStormConfig::new(
BurstPattern::MixedBurst {
count: 1000,
width: 80,
height: 24,
},
42,
);
let storm = generate_storm(&config);
let model = run_storm_in_simulator(&storm.events);
let total = model.key_count + model.mouse_count + model.paste_count + model.resize_count;
assert_eq!(total, 1000, "All events must be accounted for");
assert_eq!(model.total_events, 1000);
assert!(model.key_count > 0, "Expected some key events");
assert!(model.mouse_count > 0, "Expected some mouse events");
eprintln!(
"[mixed_burst] key={}, mouse={}, paste={}, resize={}, total={}",
model.key_count,
model.mouse_count,
model.paste_count,
model.resize_count,
model.total_events
);
}
#[test]
fn e2e_long_paste_100kb() {
let config = InputStormConfig::new(
BurstPattern::LongPaste {
size_bytes: 100_000,
},
42,
);
let storm = generate_storm(&config);
let model = run_storm_in_simulator(&storm.events);
assert_eq!(model.paste_count, 1, "Expected exactly 1 paste event");
assert_eq!(model.paste_bytes, 100_000, "Paste content must be complete");
assert_eq!(model.total_events, 1);
eprintln!("[long_paste] {} bytes processed", model.paste_bytes);
}
#[test]
fn e2e_rapid_resize_100_events() {
let config = InputStormConfig::new(BurstPattern::RapidResize { count: 100 }, 42);
let storm = generate_storm(&config);
let model = run_storm_in_simulator(&storm.events);
assert_eq!(
model.resize_count, 100,
"All resize events must be processed"
);
assert!(model.last_resize.is_some(), "Last resize must be recorded");
assert_eq!(model.total_events, 100);
eprintln!(
"[rapid_resize] {} resize events, final size={:?}",
model.resize_count, model.last_resize
);
}
#[test]
fn e2e_memory_bounded_under_sustained_burst() {
let mut sim = ProgramSimulator::new(StormTestModel::new());
sim.init();
for round in 0..10 {
let config =
InputStormConfig::new(BurstPattern::KeyboardStorm { count: 10_000 }, 42 + round);
let storm = generate_storm(&config);
sim.inject_events(&storm.events);
}
let model = sim.model();
assert_eq!(model.key_count, 100_000, "All 100K events across 10 rounds");
assert_eq!(model.total_events, 100_000);
eprintln!(
"[memory_bounded] {} total events across 10 rounds",
model.total_events
);
}
#[test]
fn e2e_storm_deterministic() {
let config = InputStormConfig::new(
BurstPattern::MixedBurst {
count: 5000,
width: 80,
height: 24,
},
12345,
);
let model1 = run_storm_in_simulator(&generate_storm(&config).events);
let model2 = run_storm_in_simulator(&generate_storm(&config).events);
assert_eq!(model1.key_count, model2.key_count);
assert_eq!(model1.mouse_count, model2.mouse_count);
assert_eq!(model1.paste_count, model2.paste_count);
assert_eq!(model1.paste_bytes, model2.paste_bytes);
assert_eq!(model1.resize_count, model2.resize_count);
assert_eq!(model1.total_events, model2.total_events);
eprintln!("[determinism] verified over 5000 mixed events");
}
#[test]
fn e2e_jsonl_evidence_complete() {
let config = InputStormConfig::new(
BurstPattern::MixedBurst {
count: 1000,
width: 80,
height: 24,
},
42,
);
let storm = generate_storm(&config);
let (_, log_lines) = run_storm_with_logging(&storm);
let start_entries: Vec<_> = log_lines
.iter()
.filter(|l| l.contains(r#""event":"storm_start""#))
.collect();
let complete_entries: Vec<_> = log_lines
.iter()
.filter(|l| l.contains(r#""event":"storm_complete""#))
.collect();
let inject_entries: Vec<_> = log_lines
.iter()
.filter(|l| l.contains(r#""event":"storm_inject""#))
.collect();
assert_eq!(start_entries.len(), 1, "Exactly 1 start entry");
assert_eq!(complete_entries.len(), 1, "Exactly 1 complete entry");
assert!(
inject_entries.len() >= 2,
"At least 2 inject samples (first + last)"
);
let complete: serde_json::Value = serde_json::from_str(complete_entries[0]).unwrap();
assert_eq!(complete["total_events"], 1000);
assert_eq!(complete["events_processed"], 1000);
eprintln!(
"[jsonl_evidence] {} log lines: {} start, {} inject, {} complete",
log_lines.len(),
start_entries.len(),
inject_entries.len(),
complete_entries.len()
);
}