1#![forbid(unsafe_code)]
2
3use std::time::Instant;
28
29use ftui_core::event::{
30 Event, KeyCode, KeyEvent, KeyEventKind, Modifiers, MouseEvent, MouseEventKind, PasteEvent,
31};
32
33#[derive(Debug, Clone, PartialEq)]
39pub enum BurstPattern {
40 KeyboardStorm {
42 count: usize,
44 },
45 MouseFlood {
47 count: usize,
49 width: u16,
51 height: u16,
53 },
54 MixedBurst {
56 count: usize,
58 width: u16,
60 height: u16,
62 },
63 LongPaste {
65 size_bytes: usize,
67 },
68 RapidResize {
70 count: usize,
72 },
73}
74
75impl BurstPattern {
76 pub fn name(&self) -> &'static str {
78 match self {
79 Self::KeyboardStorm { .. } => "keyboard_storm",
80 Self::MouseFlood { .. } => "mouse_flood",
81 Self::MixedBurst { .. } => "mixed_burst",
82 Self::LongPaste { .. } => "long_paste",
83 Self::RapidResize { .. } => "rapid_resize",
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct InputStormConfig {
91 pub pattern: BurstPattern,
93 pub seed: u64,
95}
96
97impl InputStormConfig {
98 pub fn new(pattern: BurstPattern, seed: u64) -> Self {
100 Self { pattern, seed }
101 }
102}
103
104struct Rng {
110 state: u64,
111}
112
113impl Rng {
114 fn new(seed: u64) -> Self {
115 Self {
116 state: if seed == 0 { 1 } else { seed },
117 }
118 }
119
120 fn next(&mut self) -> u64 {
121 self.state ^= self.state << 13;
122 self.state ^= self.state >> 7;
123 self.state ^= self.state << 17;
124 self.state
125 }
126
127 fn next_u16(&mut self, max: u16) -> u16 {
128 if max == 0 {
129 return 0;
130 }
131 (self.next() % max as u64) as u16
132 }
133
134 fn next_char(&mut self) -> char {
135 let idx = (self.next() % 26) as u8;
137 (b'a' + idx) as char
138 }
139}
140
141pub struct InputStorm {
143 pub events: Vec<Event>,
145 pub pattern_name: &'static str,
147 pub seed: u64,
149}
150
151pub fn generate_storm(config: &InputStormConfig) -> InputStorm {
153 let mut rng = Rng::new(config.seed);
154 let events = match &config.pattern {
155 BurstPattern::KeyboardStorm { count } => generate_keyboard_storm(*count, &mut rng),
156 BurstPattern::MouseFlood {
157 count,
158 width,
159 height,
160 } => generate_mouse_flood(*count, *width, *height, &mut rng),
161 BurstPattern::MixedBurst {
162 count,
163 width,
164 height,
165 } => generate_mixed_burst(*count, *width, *height, &mut rng),
166 BurstPattern::LongPaste { size_bytes } => generate_long_paste(*size_bytes, &mut rng),
167 BurstPattern::RapidResize { count } => generate_rapid_resize(*count, &mut rng),
168 };
169
170 InputStorm {
171 events,
172 pattern_name: config.pattern.name(),
173 seed: config.seed,
174 }
175}
176
177fn generate_keyboard_storm(count: usize, rng: &mut Rng) -> Vec<Event> {
178 let mut events = Vec::with_capacity(count);
179 for _ in 0..count {
180 let ch = rng.next_char();
181 events.push(Event::Key(KeyEvent {
182 code: KeyCode::Char(ch),
183 modifiers: Modifiers::empty(),
184 kind: KeyEventKind::Press,
185 }));
186 }
187 events
188}
189
190fn generate_mouse_flood(count: usize, width: u16, height: u16, rng: &mut Rng) -> Vec<Event> {
191 let mut events = Vec::with_capacity(count);
192 let mut x = width / 2;
193 let mut y = height / 2;
194
195 for _ in 0..count {
196 let dx = rng.next_u16(3) as i32 - 1; let dy = rng.next_u16(3) as i32 - 1;
199 x = (x as i32 + dx).clamp(0, width.saturating_sub(1) as i32) as u16;
200 y = (y as i32 + dy).clamp(0, height.saturating_sub(1) as i32) as u16;
201
202 events.push(Event::Mouse(MouseEvent {
203 kind: MouseEventKind::Moved,
204 x,
205 y,
206 modifiers: Modifiers::empty(),
207 }));
208 }
209 events
210}
211
212fn generate_mixed_burst(count: usize, width: u16, height: u16, rng: &mut Rng) -> Vec<Event> {
213 let mut events = Vec::with_capacity(count);
214 let mut mouse_x = width / 2;
215 let mut mouse_y = height / 2;
216
217 for _ in 0..count {
218 let kind = rng.next() % 10;
219 let event = match kind {
220 0..=4 => {
221 let ch = rng.next_char();
223 Event::Key(KeyEvent {
224 code: KeyCode::Char(ch),
225 modifiers: Modifiers::empty(),
226 kind: KeyEventKind::Press,
227 })
228 }
229 5..=7 => {
230 let dx = rng.next_u16(3) as i32 - 1;
232 let dy = rng.next_u16(3) as i32 - 1;
233 mouse_x = (mouse_x as i32 + dx).clamp(0, width.saturating_sub(1) as i32) as u16;
234 mouse_y = (mouse_y as i32 + dy).clamp(0, height.saturating_sub(1) as i32) as u16;
235 Event::Mouse(MouseEvent {
236 kind: MouseEventKind::Moved,
237 x: mouse_x,
238 y: mouse_y,
239 modifiers: Modifiers::empty(),
240 })
241 }
242 8 => {
243 let len = (rng.next() % 50) as usize + 5;
245 let text: String = (0..len).map(|_| rng.next_char()).collect();
246 Event::Paste(PasteEvent {
247 text,
248 bracketed: true,
249 })
250 }
251 _ => {
252 let w = rng.next_u16(120) + 20;
254 let h = rng.next_u16(50) + 10;
255 Event::Resize {
256 width: w,
257 height: h,
258 }
259 }
260 };
261 events.push(event);
262 }
263 events
264}
265
266fn generate_long_paste(size_bytes: usize, rng: &mut Rng) -> Vec<Event> {
267 let text: String = (0..size_bytes).map(|_| rng.next_char()).collect();
268 vec![Event::Paste(PasteEvent {
269 text,
270 bracketed: true,
271 })]
272}
273
274fn generate_rapid_resize(count: usize, rng: &mut Rng) -> Vec<Event> {
275 let mut events = Vec::with_capacity(count);
276 for _ in 0..count {
277 let w = rng.next_u16(120) + 20;
278 let h = rng.next_u16(50) + 10;
279 events.push(Event::Resize {
280 width: w,
281 height: h,
282 });
283 }
284 events
285}
286
287pub struct StormLogEntry {
293 pub event: &'static str,
294 pub idx: Option<usize>,
295 pub event_type: Option<&'static str>,
296 pub detail: Option<String>,
297 pub elapsed_ns: Option<u64>,
298 pub pattern: Option<&'static str>,
299 pub event_count: Option<usize>,
300 pub total_events: Option<usize>,
301 pub duration_ns: Option<u64>,
302 pub events_processed: Option<usize>,
303 pub peak_queue_depth: Option<usize>,
304 pub memory_bytes: Option<usize>,
305}
306
307impl StormLogEntry {
308 pub fn to_jsonl(&self) -> String {
309 let mut parts = vec![format!(r#""event":"{}""#, self.event)];
310 if let Some(idx) = self.idx {
311 parts.push(format!(r#""idx":{idx}"#));
312 }
313 if let Some(et) = self.event_type {
314 parts.push(format!(r#""event_type":"{et}""#));
315 }
316 if let Some(ref d) = self.detail {
317 parts.push(format!(r#""detail":"{d}""#));
318 }
319 if let Some(ns) = self.elapsed_ns {
320 parts.push(format!(r#""elapsed_ns":{ns}"#));
321 }
322 if let Some(p) = self.pattern {
323 parts.push(format!(r#""pattern":"{p}""#));
324 }
325 if let Some(c) = self.event_count {
326 parts.push(format!(r#""event_count":{c}"#));
327 }
328 if let Some(t) = self.total_events {
329 parts.push(format!(r#""total_events":{t}"#));
330 }
331 if let Some(d) = self.duration_ns {
332 parts.push(format!(r#""duration_ns":{d}"#));
333 }
334 if let Some(p) = self.events_processed {
335 parts.push(format!(r#""events_processed":{p}"#));
336 }
337 if let Some(q) = self.peak_queue_depth {
338 parts.push(format!(r#""peak_queue_depth":{q}"#));
339 }
340 if let Some(m) = self.memory_bytes {
341 parts.push(format!(r#""memory_bytes":{m}"#));
342 }
343 format!("{{{}}}", parts.join(","))
344 }
345}
346
347pub fn event_type_name(event: &Event) -> &'static str {
349 match event {
350 Event::Key(_) => "key",
351 Event::Mouse(_) => "mouse",
352 Event::Paste(_) => "paste",
353 Event::Resize { .. } => "resize",
354 Event::Focus(_) => "focus",
355 Event::Clipboard(_) => "clipboard",
356 Event::Tick => "tick",
357 }
358}
359
360pub fn run_storm_with_logging(storm: &InputStorm) -> (usize, Vec<String>) {
364 let start = Instant::now();
365 let mut log_lines = Vec::new();
366
367 log_lines.push(
369 StormLogEntry {
370 event: "storm_start",
371 pattern: Some(storm.pattern_name),
372 event_count: Some(storm.events.len()),
373 idx: None,
374 event_type: None,
375 detail: None,
376 elapsed_ns: None,
377 total_events: None,
378 duration_ns: None,
379 events_processed: None,
380 peak_queue_depth: None,
381 memory_bytes: None,
382 }
383 .to_jsonl(),
384 );
385
386 for (idx, event) in storm.events.iter().enumerate() {
388 if idx % 100 == 0 || idx == storm.events.len() - 1 {
389 let elapsed = start.elapsed().as_nanos() as u64;
390 log_lines.push(
391 StormLogEntry {
392 event: "storm_inject",
393 idx: Some(idx),
394 event_type: Some(event_type_name(event)),
395 elapsed_ns: Some(elapsed),
396 detail: None,
397 pattern: None,
398 event_count: None,
399 total_events: None,
400 duration_ns: None,
401 events_processed: None,
402 peak_queue_depth: None,
403 memory_bytes: None,
404 }
405 .to_jsonl(),
406 );
407 }
408 }
409
410 let duration = start.elapsed().as_nanos() as u64;
411 let events_processed = storm.events.len();
412
413 log_lines.push(
415 StormLogEntry {
416 event: "storm_complete",
417 total_events: Some(events_processed),
418 duration_ns: Some(duration),
419 events_processed: Some(events_processed),
420 idx: None,
421 event_type: None,
422 detail: None,
423 elapsed_ns: None,
424 pattern: None,
425 event_count: None,
426 peak_queue_depth: None,
427 memory_bytes: None,
428 }
429 .to_jsonl(),
430 );
431
432 (events_processed, log_lines)
433}
434
435#[cfg(test)]
440mod tests {
441 use super::*;
442
443 #[test]
444 fn keyboard_storm_generates_correct_count() {
445 let config = InputStormConfig::new(BurstPattern::KeyboardStorm { count: 1000 }, 42);
446 let storm = generate_storm(&config);
447 assert_eq!(storm.events.len(), 1000);
448 assert!(storm.events.iter().all(|e| matches!(e, Event::Key(_))));
449 }
450
451 #[test]
452 fn keyboard_storm_deterministic() {
453 let config = InputStormConfig::new(BurstPattern::KeyboardStorm { count: 100 }, 42);
454 let storm1 = generate_storm(&config);
455 let storm2 = generate_storm(&config);
456 assert_eq!(storm1.events.len(), storm2.events.len());
457 for (a, b) in storm1.events.iter().zip(storm2.events.iter()) {
458 assert_eq!(format!("{a:?}"), format!("{b:?}"));
459 }
460 }
461
462 #[test]
463 fn mouse_flood_generates_correct_count() {
464 let config = InputStormConfig::new(
465 BurstPattern::MouseFlood {
466 count: 1000,
467 width: 80,
468 height: 24,
469 },
470 42,
471 );
472 let storm = generate_storm(&config);
473 assert_eq!(storm.events.len(), 1000);
474 assert!(storm.events.iter().all(|e| matches!(e, Event::Mouse(_))));
475 }
476
477 #[test]
478 fn mouse_flood_stays_in_bounds() {
479 let config = InputStormConfig::new(
480 BurstPattern::MouseFlood {
481 count: 10000,
482 width: 80,
483 height: 24,
484 },
485 42,
486 );
487 let storm = generate_storm(&config);
488 for event in &storm.events {
489 if let Event::Mouse(me) = event {
490 assert!(me.x < 80, "mouse x={} out of bounds", me.x);
491 assert!(me.y < 24, "mouse y={} out of bounds", me.y);
492 }
493 }
494 }
495
496 #[test]
497 fn mixed_burst_generates_correct_count() {
498 let config = InputStormConfig::new(
499 BurstPattern::MixedBurst {
500 count: 1000,
501 width: 80,
502 height: 24,
503 },
504 42,
505 );
506 let storm = generate_storm(&config);
507 assert_eq!(storm.events.len(), 1000);
508
509 let key_count = storm
511 .events
512 .iter()
513 .filter(|e| matches!(e, Event::Key(_)))
514 .count();
515 let mouse_count = storm
516 .events
517 .iter()
518 .filter(|e| matches!(e, Event::Mouse(_)))
519 .count();
520 assert!(key_count > 0, "expected some key events");
521 assert!(mouse_count > 0, "expected some mouse events");
522 }
523
524 #[test]
525 fn long_paste_generates_correct_size() {
526 let config = InputStormConfig::new(
527 BurstPattern::LongPaste {
528 size_bytes: 100_000,
529 },
530 42,
531 );
532 let storm = generate_storm(&config);
533 assert_eq!(storm.events.len(), 1);
534 if let Event::Paste(pe) = &storm.events[0] {
535 assert_eq!(pe.text.len(), 100_000);
536 assert!(pe.bracketed);
537 } else {
538 panic!("expected paste event");
539 }
540 }
541
542 #[test]
543 fn rapid_resize_generates_correct_count() {
544 let config = InputStormConfig::new(BurstPattern::RapidResize { count: 100 }, 42);
545 let storm = generate_storm(&config);
546 assert_eq!(storm.events.len(), 100);
547 assert!(
548 storm
549 .events
550 .iter()
551 .all(|e| matches!(e, Event::Resize { .. }))
552 );
553 }
554
555 #[test]
556 fn rapid_resize_bounds() {
557 let config = InputStormConfig::new(BurstPattern::RapidResize { count: 1000 }, 42);
558 let storm = generate_storm(&config);
559 for event in &storm.events {
560 if let Event::Resize { width, height } = event {
561 assert!(*width >= 20 && *width < 140, "width={width} out of bounds");
562 assert!(
563 *height >= 10 && *height < 60,
564 "height={height} out of bounds"
565 );
566 }
567 }
568 }
569
570 #[test]
571 fn jsonl_logging_produces_valid_entries() {
572 let config = InputStormConfig::new(BurstPattern::KeyboardStorm { count: 500 }, 42);
573 let storm = generate_storm(&config);
574 let (processed, log_lines) = run_storm_with_logging(&storm);
575
576 assert_eq!(processed, 500);
577 assert!(log_lines.len() >= 3); for line in &log_lines {
581 assert!(
582 line.starts_with('{') && line.ends_with('}'),
583 "Malformed JSONL: {line}"
584 );
585 let val: serde_json::Value = serde_json::from_str(line)
587 .unwrap_or_else(|e| panic!("Failed to parse JSONL: {e}\n{line}"));
588 assert!(val["event"].is_string(), "Missing event field");
589 }
590 }
591
592 #[test]
593 fn storm_pattern_names() {
594 assert_eq!(
595 BurstPattern::KeyboardStorm { count: 1 }.name(),
596 "keyboard_storm"
597 );
598 assert_eq!(
599 BurstPattern::MouseFlood {
600 count: 1,
601 width: 80,
602 height: 24
603 }
604 .name(),
605 "mouse_flood"
606 );
607 assert_eq!(
608 BurstPattern::MixedBurst {
609 count: 1,
610 width: 80,
611 height: 24
612 }
613 .name(),
614 "mixed_burst"
615 );
616 assert_eq!(
617 BurstPattern::LongPaste { size_bytes: 1 }.name(),
618 "long_paste"
619 );
620 assert_eq!(
621 BurstPattern::RapidResize { count: 1 }.name(),
622 "rapid_resize"
623 );
624 }
625}