1use std::time::{Duration, Instant};
43
44pub type TuiLoadResult<T> = Result<T, TuiLoadError>;
46
47#[derive(Debug, Clone, PartialEq)]
49pub enum TuiLoadError {
50 FrameTimeout {
52 frame: usize,
54 timeout_ms: u64,
56 filter: String,
58 item_count: usize,
60 },
61 BudgetExceeded {
63 frame: usize,
65 actual_ms: f64,
67 budget_ms: f64,
69 },
70 DataGenerationFailed {
72 message: String,
74 },
75}
76
77impl std::fmt::Display for TuiLoadError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 Self::FrameTimeout {
81 frame,
82 timeout_ms,
83 filter,
84 item_count,
85 } => {
86 write!(
87 f,
88 "Frame {} timed out after {}ms (filter='{}', items={})",
89 frame, timeout_ms, filter, item_count
90 )
91 }
92 Self::BudgetExceeded {
93 frame,
94 actual_ms,
95 budget_ms,
96 } => {
97 write!(
98 f,
99 "Frame {} exceeded budget: {:.2}ms > {:.2}ms",
100 frame, actual_ms, budget_ms
101 )
102 }
103 Self::DataGenerationFailed { message } => {
104 write!(f, "Data generation failed: {}", message)
105 }
106 }
107 }
108}
109
110impl std::error::Error for TuiLoadError {}
111
112#[derive(Debug, Clone, Default)]
114pub struct TuiFrameMetrics {
115 pub frame_count: usize,
117 pub total_time_us: u64,
119 pub min_frame_us: u64,
121 pub max_frame_us: u64,
123 pub frame_times_us: Vec<u64>,
125}
126
127impl TuiFrameMetrics {
128 #[must_use]
130 pub fn new() -> Self {
131 Self {
132 min_frame_us: u64::MAX,
133 ..Default::default()
134 }
135 }
136
137 pub fn record(&mut self, frame_time_us: u64) {
139 self.frame_count += 1;
140 self.total_time_us += frame_time_us;
141 self.min_frame_us = self.min_frame_us.min(frame_time_us);
142 self.max_frame_us = self.max_frame_us.max(frame_time_us);
143 self.frame_times_us.push(frame_time_us);
144 }
145
146 #[must_use]
148 pub fn avg_frame_ms(&self) -> f64 {
149 if self.frame_count == 0 {
150 return 0.0;
151 }
152 (self.total_time_us as f64 / self.frame_count as f64) / 1000.0
153 }
154
155 #[must_use]
157 pub fn min_frame_ms(&self) -> f64 {
158 if self.frame_count == 0 {
159 return 0.0;
160 }
161 self.min_frame_us as f64 / 1000.0
162 }
163
164 #[must_use]
166 pub fn max_frame_ms(&self) -> f64 {
167 self.max_frame_us as f64 / 1000.0
168 }
169
170 #[must_use]
172 pub fn p50_frame_ms(&self) -> f64 {
173 self.percentile(50)
174 }
175
176 #[must_use]
178 pub fn p95_frame_ms(&self) -> f64 {
179 self.percentile(95)
180 }
181
182 #[must_use]
184 pub fn p99_frame_ms(&self) -> f64 {
185 self.percentile(99)
186 }
187
188 #[must_use]
190 pub fn percentile(&self, p: u8) -> f64 {
191 if self.frame_times_us.is_empty() {
192 return 0.0;
193 }
194 let mut sorted = self.frame_times_us.clone();
195 sorted.sort_unstable();
196 let idx = ((p as f64 / 100.0) * (sorted.len() - 1) as f64) as usize;
197 sorted[idx.min(sorted.len() - 1)] as f64 / 1000.0
198 }
199
200 #[must_use]
202 pub fn meets_fps(&self, target_fps: u32) -> bool {
203 let budget_ms = 1000.0 / target_fps as f64;
204 self.p95_frame_ms() <= budget_ms
205 }
206}
207
208#[derive(Debug, Clone)]
210pub struct SyntheticItem {
211 pub id: u32,
213 pub name: String,
215 pub description: String,
217 pub value1: f32,
219 pub value2: f32,
221 pub state: String,
223 pub owner: String,
225 pub count: u32,
227}
228
229impl SyntheticItem {
230 #[must_use]
232 pub fn matches_filter(&self, filter: &str) -> bool {
233 if filter.is_empty() {
234 return true;
235 }
236 let filter_lower = filter.to_lowercase();
237 self.name.to_lowercase().contains(&filter_lower)
238 || self.description.to_lowercase().contains(&filter_lower)
239 }
240
241 #[must_use]
243 pub fn matches_filter_precomputed(&self, filter_lower: &str) -> bool {
244 if filter_lower.is_empty() {
245 return true;
246 }
247 self.name.to_lowercase().contains(filter_lower)
248 || self.description.to_lowercase().contains(filter_lower)
249 }
250}
251
252#[derive(Debug, Clone)]
254pub struct DataGenerator {
255 seed: u64,
257 item_count: usize,
259 avg_description_len: usize,
261}
262
263impl DataGenerator {
264 #[must_use]
266 pub fn new(item_count: usize) -> Self {
267 Self {
268 seed: 42,
269 item_count,
270 avg_description_len: 100,
271 }
272 }
273
274 #[must_use]
276 pub fn with_seed(mut self, seed: u64) -> Self {
277 self.seed = seed;
278 self
279 }
280
281 #[must_use]
283 pub fn with_description_len(mut self, len: usize) -> Self {
284 self.avg_description_len = len;
285 self
286 }
287
288 #[must_use]
290 pub fn generate(&self) -> Vec<SyntheticItem> {
291 let mut items = Vec::with_capacity(self.item_count);
292 let mut rng_state = self.seed;
293
294 let names = [
295 "systemd",
296 "kworker",
297 "chrome",
298 "firefox",
299 "code",
300 "rust-analyzer",
301 "node",
302 "python",
303 "java",
304 "postgres",
305 "nginx",
306 "docker",
307 "containerd",
308 "ssh",
309 "bash",
310 "zsh",
311 "fish",
312 "vim",
313 "nvim",
314 "emacs",
315 "tmux",
316 "htop",
317 "top",
318 "ps",
319 "grep",
320 "find",
321 "cargo",
322 "rustc",
323 "gcc",
324 "clang",
325 "llvm",
326 "git",
327 "make",
328 "cmake",
329 "webpack",
330 "vite",
331 ];
332
333 let states = ["R", "S", "D", "Z", "T", "I"];
334 let users = ["root", "noah", "www-data", "postgres", "nobody", "daemon"];
335
336 for i in 0..self.item_count {
337 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
339 let r1 = rng_state;
340 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
341 let r2 = rng_state;
342 rng_state = rng_state.wrapping_mul(6364136223846793005).wrapping_add(1);
343 let r3 = rng_state;
344
345 let name_idx = (r1 as usize) % names.len();
346 let state_idx = (r2 as usize) % states.len();
347 let user_idx = (r3 as usize) % users.len();
348
349 let base_name = names[name_idx];
351 let pid = 1000 + i as u32;
352 let description = self.generate_cmdline(base_name, r1);
353
354 items.push(SyntheticItem {
355 id: pid,
356 name: format!("{}-{}", base_name, i % 100),
357 description,
358 value1: ((r1 % 10000) as f32) / 100.0, value2: ((r2 % 10000) as f32) / 100.0, state: states[state_idx].to_string(),
361 owner: users[user_idx].to_string(),
362 count: ((r3 % 64) + 1) as u32, });
364 }
365
366 items
367 }
368
369 fn generate_cmdline(&self, base_name: &str, seed: u64) -> String {
370 let args = [
371 "--config",
372 "/etc/config.yaml",
373 "--port",
374 "8080",
375 "--workers",
376 "4",
377 "--log-level",
378 "debug",
379 "--data-dir",
380 "/var/lib/data",
381 "--cache-size",
382 "1024",
383 "--timeout",
384 "30",
385 "--max-connections",
386 "1000",
387 "--enable-metrics",
388 "--prometheus-port",
389 "9090",
390 ];
391
392 let mut cmdline = format!("/usr/bin/{}", base_name);
393 let arg_count = ((seed % 6) + 2) as usize;
394
395 for i in 0..arg_count {
396 let arg_idx = ((seed.wrapping_add(i as u64 * 7)) % (args.len() as u64)) as usize;
397 cmdline.push(' ');
398 cmdline.push_str(args[arg_idx]);
399 }
400
401 while cmdline.len() < self.avg_description_len {
403 cmdline.push_str(" --extra-arg");
404 }
405
406 cmdline
407 }
408}
409
410impl Default for DataGenerator {
411 fn default() -> Self {
412 Self::new(1000)
413 }
414}
415
416#[derive(Debug, Clone)]
418pub struct TuiLoadConfig {
419 pub item_count: usize,
421 pub frame_budget_ms: f64,
423 pub timeout_ms: u64,
425 pub frames_per_filter: usize,
427 pub filters: Vec<String>,
429 pub strict_budget: bool,
431}
432
433impl Default for TuiLoadConfig {
434 fn default() -> Self {
435 Self {
436 item_count: 1000,
437 frame_budget_ms: 16.67, timeout_ms: 1000, frames_per_filter: 10,
440 filters: vec![
441 String::new(),
442 "a".to_string(),
443 "sys".to_string(),
444 "chrome".to_string(),
445 "nonexistent_filter_that_matches_nothing".to_string(),
446 ],
447 strict_budget: false,
448 }
449 }
450}
451
452#[derive(Debug)]
456pub struct TuiLoadTest {
457 config: TuiLoadConfig,
458 data: Vec<SyntheticItem>,
459}
460
461impl TuiLoadTest {
462 #[must_use]
464 pub fn new() -> Self {
465 let config = TuiLoadConfig::default();
466 let data = DataGenerator::new(config.item_count).generate();
467 Self { config, data }
468 }
469
470 #[must_use]
472 pub fn with_item_count(mut self, count: usize) -> Self {
473 self.config.item_count = count;
474 self.data = DataGenerator::new(count).generate();
475 self
476 }
477
478 #[must_use]
480 pub fn with_frame_budget_ms(mut self, budget_ms: f64) -> Self {
481 self.config.frame_budget_ms = budget_ms;
482 self
483 }
484
485 #[must_use]
487 pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
488 self.config.timeout_ms = timeout_ms;
489 self
490 }
491
492 #[must_use]
494 pub fn with_filters(mut self, filters: Vec<String>) -> Self {
495 self.config.filters = filters;
496 self
497 }
498
499 #[must_use]
501 pub fn with_frames_per_filter(mut self, count: usize) -> Self {
502 self.config.frames_per_filter = count;
503 self
504 }
505
506 #[must_use]
508 pub fn with_strict_budget(mut self, strict: bool) -> Self {
509 self.config.strict_budget = strict;
510 self
511 }
512
513 #[must_use]
515 pub fn data(&self) -> &[SyntheticItem] {
516 &self.data
517 }
518
519 #[must_use]
521 pub fn config(&self) -> &TuiLoadConfig {
522 &self.config
523 }
524
525 pub fn run<F>(&self, mut render: F) -> TuiLoadResult<TuiFrameMetrics>
540 where
541 F: FnMut(&[SyntheticItem], &str) -> Option<u64>,
542 {
543 let mut metrics = TuiFrameMetrics::new();
544 let timeout = Duration::from_millis(self.config.timeout_ms);
545 let mut frame_num = 0;
546
547 for filter in &self.config.filters {
548 for _ in 0..self.config.frames_per_filter {
549 let start = Instant::now();
550
551 let frame_time_us = if let Some(reported_time) = render(&self.data, filter) {
553 reported_time
554 } else {
555 start.elapsed().as_micros() as u64
557 };
558
559 let elapsed = start.elapsed();
560
561 if elapsed > timeout {
563 return Err(TuiLoadError::FrameTimeout {
564 frame: frame_num,
565 timeout_ms: self.config.timeout_ms,
566 filter: filter.clone(),
567 item_count: self.data.len(),
568 });
569 }
570
571 let frame_ms = frame_time_us as f64 / 1000.0;
573 if self.config.strict_budget && frame_ms > self.config.frame_budget_ms {
574 return Err(TuiLoadError::BudgetExceeded {
575 frame: frame_num,
576 actual_ms: frame_ms,
577 budget_ms: self.config.frame_budget_ms,
578 });
579 }
580
581 metrics.record(frame_time_us);
582 frame_num += 1;
583 }
584 }
585
586 Ok(metrics)
587 }
588
589 pub fn run_filter_stress<F>(
598 &self,
599 mut filter_fn: F,
600 ) -> TuiLoadResult<Vec<(String, TuiFrameMetrics)>>
601 where
602 F: FnMut(&[SyntheticItem], &str) -> Vec<SyntheticItem>,
603 {
604 let timeout = Duration::from_millis(self.config.timeout_ms);
605 let mut results = Vec::new();
606
607 let stress_filters = [
609 "",
610 "a",
611 "ab",
612 "abc",
613 "sys",
614 "syst",
615 "syste",
616 "system",
617 "systemd",
618 "chrome",
619 "rust-analyzer",
620 "this_filter_will_match_nothing_at_all",
621 ];
622
623 for filter in stress_filters {
624 let mut metrics = TuiFrameMetrics::new();
625
626 for frame in 0..self.config.frames_per_filter {
627 let start = Instant::now();
628
629 let _filtered = filter_fn(&self.data, filter);
631
632 let elapsed = start.elapsed();
633
634 if elapsed > timeout {
636 return Err(TuiLoadError::FrameTimeout {
637 frame,
638 timeout_ms: self.config.timeout_ms,
639 filter: filter.to_string(),
640 item_count: self.data.len(),
641 });
642 }
643
644 metrics.record(elapsed.as_micros() as u64);
645 }
646
647 results.push((filter.to_string(), metrics));
648 }
649
650 Ok(results)
651 }
652}
653
654impl Default for TuiLoadTest {
655 fn default() -> Self {
656 Self::new()
657 }
658}
659
660#[derive(Debug, Clone, Copy, Default)]
662pub struct TuiLoadAssertion;
663
664impl TuiLoadAssertion {
665 pub fn assert_meets_fps(metrics: &TuiFrameMetrics, target_fps: u32) {
667 let budget_ms = 1000.0 / target_fps as f64;
668 assert!(
669 metrics.p95_frame_ms() <= budget_ms,
670 "p95 frame time {:.2}ms exceeds {:.2}ms budget for {} FPS",
671 metrics.p95_frame_ms(),
672 budget_ms,
673 target_fps
674 );
675 }
676
677 pub fn assert_no_hang(result: &TuiLoadResult<TuiFrameMetrics>) {
679 assert!(
680 result.is_ok(),
681 "TUI hang detected: {:?}",
682 result.as_ref().err()
683 );
684 }
685
686 pub fn assert_filter_scales_linearly(
688 results: &[(String, TuiFrameMetrics)],
689 max_degradation_factor: f64,
690 ) {
691 if results.len() < 2 {
692 return;
693 }
694
695 let baseline = results[0].1.avg_frame_ms();
696 if baseline == 0.0 {
697 return;
698 }
699
700 for (filter, metrics) in results.iter().skip(1) {
701 let factor = metrics.avg_frame_ms() / baseline;
702 assert!(
703 factor <= max_degradation_factor,
704 "Filter '{}' degraded by {:.1}x (max allowed: {:.1}x)",
705 filter,
706 factor,
707 max_degradation_factor
708 );
709 }
710 }
711}
712
713#[derive(Debug, Clone)]
740pub struct IntegrationLoadTest {
741 frame_budget_ms: f64,
743 timeout_ms: u64,
745 frame_count: usize,
747 component_budgets: std::collections::HashMap<String, f64>,
749}
750
751impl IntegrationLoadTest {
752 #[must_use]
754 pub fn new() -> Self {
755 Self {
756 frame_budget_ms: 100.0, timeout_ms: 5000, frame_count: 5,
759 component_budgets: std::collections::HashMap::new(),
760 }
761 }
762
763 #[must_use]
765 pub fn with_frame_budget_ms(mut self, budget: f64) -> Self {
766 self.frame_budget_ms = budget;
767 self
768 }
769
770 #[must_use]
772 pub fn with_timeout_ms(mut self, timeout: u64) -> Self {
773 self.timeout_ms = timeout;
774 self
775 }
776
777 #[must_use]
779 pub fn with_frame_count(mut self, count: usize) -> Self {
780 self.frame_count = count;
781 self
782 }
783
784 #[must_use]
786 pub fn with_component_budget(mut self, name: &str, max_ms: f64) -> Self {
787 self.component_budgets.insert(name.to_string(), max_ms);
788 self
789 }
790
791 pub fn run<F>(&self, mut frame_fn: F) -> TuiLoadResult<TuiFrameMetrics>
800 where
801 F: FnMut() -> ComponentTimings,
802 {
803 let mut metrics = TuiFrameMetrics::new();
804 let timeout = Duration::from_millis(self.timeout_ms);
805
806 for frame in 0..self.frame_count {
807 let start = Instant::now();
808
809 let timings = frame_fn();
810
811 let elapsed = start.elapsed();
812
813 if elapsed > timeout {
815 return Err(TuiLoadError::FrameTimeout {
816 frame,
817 timeout_ms: self.timeout_ms,
818 filter: format!("frame {}", frame),
819 item_count: 0,
820 });
821 }
822
823 for (name, &max_ms) in &self.component_budgets {
825 if let Some(&actual_ms) = timings.0.get(name) {
826 if actual_ms > max_ms {
827 return Err(TuiLoadError::BudgetExceeded {
828 frame,
829 actual_ms,
830 budget_ms: max_ms,
831 });
832 }
833 }
834 }
835
836 metrics.record(elapsed.as_micros() as u64);
837 }
838
839 Ok(metrics)
840 }
841}
842
843impl Default for IntegrationLoadTest {
844 fn default() -> Self {
845 Self::new()
846 }
847}
848
849#[derive(Debug, Clone, Default)]
851pub struct ComponentTimings(pub std::collections::HashMap<String, f64>);
852
853impl ComponentTimings {
854 #[must_use]
856 pub fn new() -> Self {
857 Self(std::collections::HashMap::new())
858 }
859
860 pub fn record(&mut self, name: &str, duration_ms: f64) {
862 self.0.insert(name.to_string(), duration_ms);
863 }
864
865 #[must_use]
867 pub fn get(&self, name: &str) -> Option<f64> {
868 self.0.get(name).copied()
869 }
870}
871
872#[cfg(test)]
873mod tests {
874 use super::*;
875
876 #[test]
877 fn test_data_generator_creates_items() {
878 let gen = DataGenerator::new(100);
879 let items = gen.generate();
880 assert_eq!(items.len(), 100);
881 }
882
883 #[test]
884 fn test_data_generator_deterministic() {
885 let gen1 = DataGenerator::new(50).with_seed(12345);
886 let gen2 = DataGenerator::new(50).with_seed(12345);
887 let items1 = gen1.generate();
888 let items2 = gen2.generate();
889
890 for (a, b) in items1.iter().zip(items2.iter()) {
891 assert_eq!(a.id, b.id);
892 assert_eq!(a.name, b.name);
893 }
894 }
895
896 #[test]
897 fn test_synthetic_item_filter_empty() {
898 let item = SyntheticItem {
899 id: 1,
900 name: "test".to_string(),
901 description: "desc".to_string(),
902 value1: 0.0,
903 value2: 0.0,
904 state: "R".to_string(),
905 owner: "root".to_string(),
906 count: 1,
907 };
908 assert!(item.matches_filter(""));
909 }
910
911 #[test]
912 fn test_synthetic_item_filter_name() {
913 let item = SyntheticItem {
914 id: 1,
915 name: "systemd".to_string(),
916 description: "init system".to_string(),
917 value1: 0.0,
918 value2: 0.0,
919 state: "S".to_string(),
920 owner: "root".to_string(),
921 count: 1,
922 };
923 assert!(item.matches_filter("sys"));
924 assert!(item.matches_filter("SYS")); assert!(!item.matches_filter("chrome"));
926 }
927
928 #[test]
929 fn test_synthetic_item_filter_description() {
930 let item = SyntheticItem {
931 id: 1,
932 name: "init".to_string(),
933 description: "/usr/lib/systemd/systemd".to_string(),
934 value1: 0.0,
935 value2: 0.0,
936 state: "S".to_string(),
937 owner: "root".to_string(),
938 count: 1,
939 };
940 assert!(item.matches_filter("systemd"));
941 }
942
943 #[test]
944 fn test_frame_metrics_percentiles() {
945 let mut metrics = TuiFrameMetrics::new();
946 for i in 1..=100 {
947 metrics.record(i * 1000); }
949
950 assert_eq!(metrics.frame_count, 100);
951 assert!((metrics.p50_frame_ms() - 50.0).abs() < 2.0);
952 assert!(metrics.p95_frame_ms() >= 95.0);
953 }
954
955 #[test]
956 fn test_frame_metrics_meets_fps() {
957 let mut metrics = TuiFrameMetrics::new();
958 for _ in 0..100 {
960 metrics.record(10_000); }
962
963 assert!(metrics.meets_fps(60)); assert!(metrics.meets_fps(100)); assert!(!metrics.meets_fps(120)); }
967
968 #[test]
969 fn test_tui_load_test_no_hang() {
970 let test = TuiLoadTest::new()
971 .with_item_count(100)
972 .with_timeout_ms(1000);
973
974 let result = test.run(|_items, _filter| {
975 Some(100) });
978
979 assert!(result.is_ok());
980 let metrics = result.unwrap();
981 assert!(metrics.frame_count > 0);
982 }
983
984 #[test]
985 fn test_tui_load_test_detects_hang() {
986 let test = TuiLoadTest::new()
987 .with_item_count(10)
988 .with_timeout_ms(50) .with_frames_per_filter(1);
990
991 let result = test.run(|_items, _filter| {
992 std::thread::sleep(Duration::from_millis(100));
994 None
995 });
996
997 assert!(result.is_err());
998 match result {
999 Err(TuiLoadError::FrameTimeout { .. }) => {}
1000 _ => panic!("Expected FrameTimeout error"),
1001 }
1002 }
1003
1004 #[test]
1005 fn test_tui_load_test_large_dataset() {
1006 let test = TuiLoadTest::new()
1007 .with_item_count(5000)
1008 .with_timeout_ms(5000)
1009 .with_frames_per_filter(3);
1010
1011 let result = test.run(|items, filter| {
1013 let filter_lower = filter.to_lowercase();
1014 let _filtered: Vec<_> = items
1015 .iter()
1016 .filter(|item| item.matches_filter_precomputed(&filter_lower))
1017 .collect();
1018 None });
1020
1021 assert!(result.is_ok(), "Should handle 5000 items without hang");
1022 let metrics = result.unwrap();
1023
1024 assert!(
1026 metrics.p95_frame_ms() < 100.0,
1027 "p95 = {:.2}ms, should be < 100ms",
1028 metrics.p95_frame_ms()
1029 );
1030 }
1031
1032 #[test]
1033 fn test_filter_stress_test() {
1034 let test = TuiLoadTest::new()
1035 .with_item_count(1000)
1036 .with_timeout_ms(2000)
1037 .with_frames_per_filter(5);
1038
1039 let result = test.run_filter_stress(|items, filter| {
1040 let filter_lower = filter.to_lowercase();
1041 items
1042 .iter()
1043 .filter(|item| item.matches_filter_precomputed(&filter_lower))
1044 .cloned()
1045 .collect()
1046 });
1047
1048 assert!(result.is_ok());
1049 let results = result.unwrap();
1050
1051 assert!(!results.is_empty());
1053
1054 TuiLoadAssertion::assert_filter_scales_linearly(&results, 5.0);
1056 }
1057
1058 #[test]
1059 fn test_tui_load_error_display() {
1060 let err = TuiLoadError::FrameTimeout {
1061 frame: 5,
1062 timeout_ms: 1000,
1063 filter: "test".to_string(),
1064 item_count: 5000,
1065 };
1066 let msg = err.to_string();
1067 assert!(msg.contains('5'));
1068 assert!(msg.contains("1000"));
1069 assert!(msg.contains("test"));
1070 assert!(msg.contains("5000"));
1071 }
1072
1073 #[test]
1074 fn test_data_generator_with_long_descriptions() {
1075 let gen = DataGenerator::new(10).with_description_len(200);
1076 let items = gen.generate();
1077
1078 for item in &items {
1080 assert!(
1081 item.description.len() >= 100,
1082 "Description too short: {}",
1083 item.description.len()
1084 );
1085 }
1086 }
1087
1088 #[test]
1089 fn test_data_generator_default() {
1090 let gen = DataGenerator::default();
1091 let items = gen.generate();
1092 assert_eq!(items.len(), 1000);
1093 }
1094
1095 #[test]
1096 fn test_integration_load_test_run_success() {
1097 let test = IntegrationLoadTest::new()
1098 .with_frame_budget_ms(500.0)
1099 .with_timeout_ms(2000)
1100 .with_frame_count(3);
1101
1102 let mut call_count = 0;
1103 let result = test.run(|| {
1104 call_count += 1;
1105 let mut timings = ComponentTimings::new();
1106 timings.record("render", 1.0);
1107 timings
1108 });
1109
1110 assert!(result.is_ok());
1111 let metrics = result.unwrap();
1112 assert_eq!(metrics.frame_count, 3);
1113 assert_eq!(call_count, 3);
1114 }
1115
1116 #[test]
1117 fn test_integration_load_test_default() {
1118 let test = IntegrationLoadTest::default();
1119 let result = test.run(|| ComponentTimings::new());
1120 assert!(result.is_ok());
1121 }
1122
1123 #[test]
1124 fn test_integration_load_test_budget_exceeded() {
1125 let test = IntegrationLoadTest::new()
1126 .with_frame_count(2)
1127 .with_timeout_ms(5000)
1128 .with_component_budget("slow_component", 0.001); let result = test.run(|| {
1131 let mut timings = ComponentTimings::new();
1132 timings.record("slow_component", 10.0); timings
1134 });
1135
1136 assert!(result.is_err());
1137 match result {
1138 Err(TuiLoadError::BudgetExceeded {
1139 actual_ms,
1140 budget_ms,
1141 ..
1142 }) => {
1143 assert!((actual_ms - 10.0).abs() < f64::EPSILON);
1144 assert!((budget_ms - 0.001).abs() < f64::EPSILON);
1145 }
1146 _ => panic!("Expected BudgetExceeded error"),
1147 }
1148 }
1149
1150 #[test]
1151 fn test_integration_load_test_component_within_budget() {
1152 let test = IntegrationLoadTest::new()
1153 .with_frame_count(2)
1154 .with_timeout_ms(5000)
1155 .with_component_budget("fast", 100.0);
1156
1157 let result = test.run(|| {
1158 let mut timings = ComponentTimings::new();
1159 timings.record("fast", 1.0);
1160 timings
1161 });
1162
1163 assert!(result.is_ok());
1164 }
1165
1166 #[test]
1167 fn test_component_timings() {
1168 let mut t = ComponentTimings::new();
1169 assert!(t.get("render").is_none());
1170
1171 t.record("render", 5.5);
1172 assert!((t.get("render").unwrap() - 5.5).abs() < f64::EPSILON);
1173
1174 t.record("layout", 2.0);
1175 assert!((t.get("layout").unwrap() - 2.0).abs() < f64::EPSILON);
1176 }
1177
1178 #[test]
1179 fn test_tui_load_config_builder() {
1180 let config = TuiLoadConfig {
1181 item_count: 500,
1182 frame_budget_ms: 32.0,
1183 timeout_ms: 3000,
1184 frames_per_filter: 10,
1185 filters: vec!["sys".to_string(), "usr".to_string()],
1186 strict_budget: true,
1187 };
1188 assert_eq!(config.item_count, 500);
1189 assert!((config.frame_budget_ms - 32.0).abs() < f64::EPSILON);
1190 assert_eq!(config.timeout_ms, 3000);
1191 assert_eq!(config.frames_per_filter, 10);
1192 assert_eq!(config.filters.len(), 2);
1193 assert!(config.strict_budget);
1194 }
1195
1196 #[test]
1197 fn test_tui_load_error_budget_exceeded_display() {
1198 let err = TuiLoadError::BudgetExceeded {
1199 frame: 3,
1200 actual_ms: 150.0,
1201 budget_ms: 16.6,
1202 };
1203 let msg = err.to_string();
1204 assert!(msg.contains("150"));
1205 assert!(msg.contains("16.6"));
1206 }
1207
1208 #[test]
1209 fn test_frame_metrics_empty() {
1210 let metrics = TuiFrameMetrics::new();
1211 assert_eq!(metrics.frame_count, 0);
1212 assert_eq!(metrics.p50_frame_ms(), 0.0);
1213 assert_eq!(metrics.p95_frame_ms(), 0.0);
1214 }
1215}