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 run_once = || {
1010 TuiLoadTest::new()
1011 .with_item_count(5000)
1012 .with_timeout_ms(5000)
1013 .with_frames_per_filter(3)
1014 .run(|items, filter| {
1015 let filter_lower = filter.to_lowercase();
1016 let _filtered: Vec<_> = items
1017 .iter()
1018 .filter(|item| item.matches_filter_precomputed(&filter_lower))
1019 .collect();
1020 None
1021 })
1022 };
1023
1024 let _warmup = run_once();
1025
1026 let mut best_p95 = f64::INFINITY;
1027 for _ in 0..3 {
1028 let result = run_once();
1029 assert!(result.is_ok(), "Should handle 5000 items without hang");
1030 let p95 = result.unwrap().p95_frame_ms();
1031 if p95 < best_p95 {
1032 best_p95 = p95;
1033 }
1034 }
1035
1036 assert!(
1037 best_p95 < 100.0,
1038 "p95 (min of 3) = {best_p95:.2}ms, should be < 100ms"
1039 );
1040 }
1041
1042 #[test]
1043 fn test_filter_stress_test() {
1044 let test = TuiLoadTest::new()
1045 .with_item_count(1000)
1046 .with_timeout_ms(2000)
1047 .with_frames_per_filter(5);
1048
1049 let result = test.run_filter_stress(|items, filter| {
1050 let filter_lower = filter.to_lowercase();
1051 items
1052 .iter()
1053 .filter(|item| item.matches_filter_precomputed(&filter_lower))
1054 .cloned()
1055 .collect()
1056 });
1057
1058 assert!(result.is_ok(), "filter stress test must not error");
1059 let results = result.expect("checked above");
1060
1061 assert!(!results.is_empty(), "must produce at least one result");
1063
1064 }
1067
1068 #[test]
1069 fn test_tui_load_error_display() {
1070 let err = TuiLoadError::FrameTimeout {
1071 frame: 5,
1072 timeout_ms: 1000,
1073 filter: "test".to_string(),
1074 item_count: 5000,
1075 };
1076 let msg = err.to_string();
1077 assert!(msg.contains('5'));
1078 assert!(msg.contains("1000"));
1079 assert!(msg.contains("test"));
1080 assert!(msg.contains("5000"));
1081 }
1082
1083 #[test]
1084 fn test_data_generator_with_long_descriptions() {
1085 let gen = DataGenerator::new(10).with_description_len(200);
1086 let items = gen.generate();
1087
1088 for item in &items {
1090 assert!(
1091 item.description.len() >= 100,
1092 "Description too short: {}",
1093 item.description.len()
1094 );
1095 }
1096 }
1097
1098 #[test]
1099 fn test_data_generator_default() {
1100 let gen = DataGenerator::default();
1101 let items = gen.generate();
1102 assert_eq!(items.len(), 1000);
1103 }
1104
1105 #[test]
1106 fn test_integration_load_test_run_success() {
1107 let test = IntegrationLoadTest::new()
1108 .with_frame_budget_ms(500.0)
1109 .with_timeout_ms(2000)
1110 .with_frame_count(3);
1111
1112 let mut call_count = 0;
1113 let result = test.run(|| {
1114 call_count += 1;
1115 let mut timings = ComponentTimings::new();
1116 timings.record("render", 1.0);
1117 timings
1118 });
1119
1120 assert!(result.is_ok());
1121 let metrics = result.unwrap();
1122 assert_eq!(metrics.frame_count, 3);
1123 assert_eq!(call_count, 3);
1124 }
1125
1126 #[test]
1127 fn test_integration_load_test_default() {
1128 let test = IntegrationLoadTest::default();
1129 let result = test.run(|| ComponentTimings::new());
1130 assert!(result.is_ok());
1131 }
1132
1133 #[test]
1134 fn test_integration_load_test_budget_exceeded() {
1135 let test = IntegrationLoadTest::new()
1136 .with_frame_count(2)
1137 .with_timeout_ms(5000)
1138 .with_component_budget("slow_component", 0.001); let result = test.run(|| {
1141 let mut timings = ComponentTimings::new();
1142 timings.record("slow_component", 10.0); timings
1144 });
1145
1146 assert!(result.is_err());
1147 match result {
1148 Err(TuiLoadError::BudgetExceeded {
1149 actual_ms,
1150 budget_ms,
1151 ..
1152 }) => {
1153 assert!((actual_ms - 10.0).abs() < f64::EPSILON);
1154 assert!((budget_ms - 0.001).abs() < f64::EPSILON);
1155 }
1156 _ => panic!("Expected BudgetExceeded error"),
1157 }
1158 }
1159
1160 #[test]
1161 fn test_integration_load_test_component_within_budget() {
1162 let test = IntegrationLoadTest::new()
1163 .with_frame_count(2)
1164 .with_timeout_ms(5000)
1165 .with_component_budget("fast", 100.0);
1166
1167 let result = test.run(|| {
1168 let mut timings = ComponentTimings::new();
1169 timings.record("fast", 1.0);
1170 timings
1171 });
1172
1173 assert!(result.is_ok());
1174 }
1175
1176 #[test]
1177 fn test_component_timings() {
1178 let mut t = ComponentTimings::new();
1179 assert!(t.get("render").is_none());
1180
1181 t.record("render", 5.5);
1182 assert!((t.get("render").unwrap() - 5.5).abs() < f64::EPSILON);
1183
1184 t.record("layout", 2.0);
1185 assert!((t.get("layout").unwrap() - 2.0).abs() < f64::EPSILON);
1186 }
1187
1188 #[test]
1189 fn test_tui_load_config_builder() {
1190 let config = TuiLoadConfig {
1191 item_count: 500,
1192 frame_budget_ms: 32.0,
1193 timeout_ms: 3000,
1194 frames_per_filter: 10,
1195 filters: vec!["sys".to_string(), "usr".to_string()],
1196 strict_budget: true,
1197 };
1198 assert_eq!(config.item_count, 500);
1199 assert!((config.frame_budget_ms - 32.0).abs() < f64::EPSILON);
1200 assert_eq!(config.timeout_ms, 3000);
1201 assert_eq!(config.frames_per_filter, 10);
1202 assert_eq!(config.filters.len(), 2);
1203 assert!(config.strict_budget);
1204 }
1205
1206 #[test]
1207 fn test_tui_load_error_budget_exceeded_display() {
1208 let err = TuiLoadError::BudgetExceeded {
1209 frame: 3,
1210 actual_ms: 150.0,
1211 budget_ms: 16.6,
1212 };
1213 let msg = err.to_string();
1214 assert!(msg.contains("150"));
1215 assert!(msg.contains("16.6"));
1216 }
1217
1218 #[test]
1219 fn test_frame_metrics_empty() {
1220 let metrics = TuiFrameMetrics::new();
1221 assert_eq!(metrics.frame_count, 0);
1222 assert_eq!(metrics.p50_frame_ms(), 0.0);
1223 assert_eq!(metrics.p95_frame_ms(), 0.0);
1224 }
1225}