1use super::stress::{PerformanceResult, StressReport};
14
15#[derive(Debug, Clone)]
17pub struct TuiConfig {
18 pub refresh_rate_ms: u64,
20 pub show_frame_times: bool,
22 pub show_memory_usage: bool,
24 pub show_anomaly_alerts: bool,
26 pub title: String,
28}
29
30impl Default for TuiConfig {
31 fn default() -> Self {
32 Self {
33 refresh_rate_ms: 100,
34 show_frame_times: true,
35 show_memory_usage: true,
36 show_anomaly_alerts: true,
37 title: "trueno-gpu Stress Test Monitor".to_string(),
38 }
39 }
40}
41
42#[derive(Debug, Clone, Default)]
44pub struct TuiState {
45 pub current_cycle: u32,
47 pub total_cycles: u32,
49 pub current_fps: f64,
51 pub memory_bytes: usize,
53 pub frame_times: Vec<u64>,
55 pub test_results: Vec<(String, u64, bool)>, pub anomaly_count: usize,
59 pub regression_count: usize,
61 pub pass_rate: f64,
63 pub running: bool,
65 pub paused: bool,
67}
68
69impl TuiState {
70 #[must_use]
72 pub fn new(total_cycles: u32) -> Self {
73 Self {
74 total_cycles,
75 running: true,
76 ..Default::default()
77 }
78 }
79
80 pub fn update_from_report(&mut self, report: &StressReport) {
82 self.current_cycle = report.cycles_completed;
83 self.anomaly_count = report.anomalies.len();
84 self.pass_rate = report.pass_rate();
85
86 self.frame_times = report
88 .frames
89 .iter()
90 .rev()
91 .take(50)
92 .map(|f| f.duration_ms)
93 .collect();
94 self.frame_times.reverse();
95
96 let mean_ms = report.mean_frame_time_ms();
98 self.current_fps = if mean_ms > 0.0 { 1000.0 / mean_ms } else { 0.0 };
99
100 if let Some(last) = report.frames.last() {
102 self.memory_bytes = last.memory_bytes;
103 }
104 }
105
106 #[must_use]
108 pub fn format_memory(&self) -> String {
109 let bytes = self.memory_bytes as f64;
110 if bytes < 1024.0 {
111 format!("{:.0} B", bytes)
112 } else if bytes < 1024.0 * 1024.0 {
113 format!("{:.1} KB", bytes / 1024.0)
114 } else {
115 format!("{:.1} MB", bytes / (1024.0 * 1024.0))
116 }
117 }
118
119 #[must_use]
121 pub fn sparkline_data(&self) -> Vec<u8> {
122 if self.frame_times.is_empty() {
123 return vec![];
124 }
125
126 let max = *self.frame_times.iter().max().unwrap_or(&1) as f64;
127 let min = *self.frame_times.iter().min().unwrap_or(&0) as f64;
128 let range = (max - min).max(1.0);
129
130 self.frame_times
131 .iter()
132 .map(|&t| {
133 let normalized = (t as f64 - min) / range;
134 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
136 let level = (normalized * 7.0).round() as u8;
137 level
138 })
139 .collect()
140 }
141
142 #[must_use]
144 pub fn sparkline_string(&self) -> String {
145 const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
146 self.sparkline_data()
147 .iter()
148 .map(|&v| BLOCKS[v.min(7) as usize])
149 .collect()
150 }
151}
152
153#[must_use]
155pub fn render_to_string(
156 state: &TuiState,
157 report: &StressReport,
158 perf: &PerformanceResult,
159) -> String {
160 let mut output = String::new();
161
162 output.push_str("╔══════════════════════════════════════════════════════════════╗\n");
164 output.push_str("║ trueno-gpu Stress Test Monitor (simular TUI) ║\n");
165 output.push_str("╠══════════════════════════════════════════════════════════════╣\n");
166
167 output.push_str(&format!(
169 "║ Cycle: {}/{} FPS: {:.1} Memory: {:<10} ║\n",
170 state.current_cycle,
171 state.total_cycles,
172 state.current_fps,
173 state.format_memory()
174 ));
175 output.push_str("║ ║\n");
176
177 let sparkline = state.sparkline_string();
179 if !sparkline.is_empty() {
180 output.push_str(&format!("║ Frame Times (ms): {:<40} ║\n", sparkline));
181 }
182
183 output.push_str(&format!(
185 "║ Mean: {:.0}ms Max: {}ms Variance: {:.2} ║\n",
186 perf.mean_frame_ms, perf.max_frame_ms, perf.variance
187 ));
188 output.push_str("║ ║\n");
189
190 output.push_str("║ Test Results: ║\n");
192 let passed = report.total_passed;
193 let failed = report.total_failed;
194 output.push_str(&format!(
195 "║ ✓ Passed: {:<6} ✗ Failed: {:<6} ║\n",
196 passed, failed
197 ));
198 output.push_str("║ ║\n");
199
200 let status = if perf.passed { "PASS" } else { "FAIL" };
202 output.push_str(&format!(
203 "║ Anomalies: {} Regressions: {} Status: {:<4} ║\n",
204 state.anomaly_count, state.regression_count, status
205 ));
206
207 output.push_str("╠══════════════════════════════════════════════════════════════╣\n");
209 output.push_str("║ [q] Quit [p] Pause [r] Reset [s] Save Report ║\n");
210 output.push_str("╚══════════════════════════════════════════════════════════════╝\n");
211
212 output
213}
214
215#[must_use]
217pub fn progress_bar(current: u32, total: u32, width: usize) -> String {
218 if total == 0 {
219 return format!("[{}]", " ".repeat(width));
220 }
221
222 let progress = (current as f64 / total as f64).min(1.0);
223 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
225 let filled = (progress * width as f64).round() as usize;
226 let empty = width - filled;
227
228 format!("[{}{}]", "█".repeat(filled), "░".repeat(empty))
229}
230
231#[cfg(feature = "tui-monitor")]
232pub mod interactive {
233 #[allow(clippy::wildcard_imports)]
238 use super::*;
239
240 pub fn run_interactive(
246 _config: TuiConfig,
247 _state: &mut TuiState,
248 ) -> Result<(), Box<dyn std::error::Error>> {
249 Err("Interactive TUI requires tui-monitor feature with presentar".into())
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_tui_config_default() {
261 let config = TuiConfig::default();
262 assert_eq!(config.refresh_rate_ms, 100);
263 assert!(config.show_frame_times);
264 assert!(config.show_memory_usage);
265 assert!(config.show_anomaly_alerts);
266 }
267
268 #[test]
269 fn test_tui_state_new() {
270 let state = TuiState::new(100);
271 assert_eq!(state.total_cycles, 100);
272 assert!(state.running);
273 assert!(!state.paused);
274 }
275
276 #[test]
277 fn test_format_memory() {
278 let mut state = TuiState::default();
279
280 state.memory_bytes = 512;
281 assert_eq!(state.format_memory(), "512 B");
282
283 state.memory_bytes = 2048;
284 assert_eq!(state.format_memory(), "2.0 KB");
285
286 state.memory_bytes = 5 * 1024 * 1024;
287 assert_eq!(state.format_memory(), "5.0 MB");
288 }
289
290 #[test]
291 fn test_sparkline_data() {
292 let mut state = TuiState::default();
293 state.frame_times = vec![10, 20, 30, 40, 50];
294
295 let data = state.sparkline_data();
296 assert_eq!(data.len(), 5);
297 assert_eq!(data[0], 0); assert_eq!(data[4], 7); }
300
301 #[test]
302 fn test_sparkline_string() {
303 let mut state = TuiState::default();
304 state.frame_times = vec![10, 20, 30, 40, 50];
305
306 let sparkline = state.sparkline_string();
307 assert_eq!(sparkline.chars().count(), 5);
308 assert!(sparkline.starts_with('▁'));
309 assert!(sparkline.ends_with('█'));
310 }
311
312 #[test]
313 fn test_progress_bar() {
314 assert_eq!(progress_bar(0, 100, 10), "[░░░░░░░░░░]");
315 assert_eq!(progress_bar(50, 100, 10), "[█████░░░░░]");
316 assert_eq!(progress_bar(100, 100, 10), "[██████████]");
317 assert_eq!(progress_bar(0, 0, 10), "[ ]"); }
319
320 #[test]
321 fn test_render_to_string() {
322 let state = TuiState::new(100);
323 let report = StressReport::default();
324 let perf = PerformanceResult {
325 passed: true,
326 max_frame_ms: 50,
327 mean_frame_ms: 40.0,
328 variance: 0.1,
329 pass_rate: 1.0,
330 violations: vec![],
331 };
332
333 let output = render_to_string(&state, &report, &perf);
334 assert!(output.contains("trueno-gpu Stress Test Monitor"));
335 assert!(output.contains("Cycle: 0/100"));
336 assert!(output.contains("PASS"));
337 }
338
339 #[test]
340 fn test_update_from_report() {
341 use super::super::stress::{FrameProfile, StressReport};
342
343 let mut state = TuiState::new(10);
344 let mut report = StressReport::default();
345
346 for i in 0..5 {
347 report.add_frame(FrameProfile {
348 cycle: i,
349 duration_ms: 50 + i as u64 * 10,
350 memory_bytes: 1024,
351 tests_passed: 5,
352 tests_failed: 0,
353 ..Default::default()
354 });
355 }
356
357 state.update_from_report(&report);
358
359 assert_eq!(state.current_cycle, 5);
360 assert_eq!(state.frame_times.len(), 5);
361 assert!(state.current_fps > 0.0);
362 assert!((state.pass_rate - 1.0).abs() < 0.001);
363 }
364}