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