use super::stress::{PerformanceResult, StressReport};
#[derive(Debug, Clone)]
pub struct TuiConfig {
pub refresh_rate_ms: u64,
pub show_frame_times: bool,
pub show_memory_usage: bool,
pub show_anomaly_alerts: bool,
pub title: String,
}
impl Default for TuiConfig {
fn default() -> Self {
Self {
refresh_rate_ms: 100,
show_frame_times: true,
show_memory_usage: true,
show_anomaly_alerts: true,
title: "trueno-gpu Stress Test Monitor".to_string(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TuiState {
pub current_cycle: u32,
pub total_cycles: u32,
pub current_fps: f64,
pub memory_bytes: usize,
pub frame_times: Vec<u64>,
pub test_results: Vec<(String, u64, bool)>, pub anomaly_count: usize,
pub regression_count: usize,
pub pass_rate: f64,
pub running: bool,
pub paused: bool,
}
impl TuiState {
#[must_use]
pub fn new(total_cycles: u32) -> Self {
Self { total_cycles, running: true, ..Default::default() }
}
pub fn update_from_report(&mut self, report: &StressReport) {
self.current_cycle = report.cycles_completed;
self.anomaly_count = report.anomalies.len();
self.pass_rate = report.pass_rate();
self.frame_times = report.frames.iter().rev().take(50).map(|f| f.duration_ms).collect();
self.frame_times.reverse();
let mean_ms = report.mean_frame_time_ms();
self.current_fps = if mean_ms > 0.0 { 1000.0 / mean_ms } else { 0.0 };
if let Some(last) = report.frames.last() {
self.memory_bytes = last.memory_bytes;
}
}
#[must_use]
pub fn format_memory(&self) -> String {
let bytes = self.memory_bytes as f64;
if bytes < 1024.0 {
format!("{:.0} B", bytes)
} else if bytes < 1024.0 * 1024.0 {
format!("{:.1} KB", bytes / 1024.0)
} else {
format!("{:.1} MB", bytes / (1024.0 * 1024.0))
}
}
#[must_use]
pub fn sparkline_data(&self) -> Vec<u8> {
if self.frame_times.is_empty() {
return vec![];
}
let max = *self.frame_times.iter().max().unwrap_or(&1) as f64;
let min = *self.frame_times.iter().min().unwrap_or(&0) as f64;
let range = (max - min).max(1.0);
self.frame_times
.iter()
.map(|&t| {
let normalized = (t as f64 - min) / range;
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let level = (normalized * 7.0).round() as u8;
level
})
.collect()
}
#[must_use]
pub fn sparkline_string(&self) -> String {
const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
self.sparkline_data().iter().map(|&v| BLOCKS[v.min(7) as usize]).collect()
}
}
#[must_use]
pub fn render_to_string(
state: &TuiState,
report: &StressReport,
perf: &PerformanceResult,
) -> String {
let mut output = String::new();
output.push_str("╔══════════════════════════════════════════════════════════════╗\n");
output.push_str("║ trueno-gpu Stress Test Monitor (simular TUI) ║\n");
output.push_str("╠══════════════════════════════════════════════════════════════╣\n");
output.push_str(&format!(
"║ Cycle: {}/{} FPS: {:.1} Memory: {:<10} ║\n",
state.current_cycle,
state.total_cycles,
state.current_fps,
state.format_memory()
));
output.push_str("║ ║\n");
let sparkline = state.sparkline_string();
if !sparkline.is_empty() {
output.push_str(&format!("║ Frame Times (ms): {:<40} ║\n", sparkline));
}
output.push_str(&format!(
"║ Mean: {:.0}ms Max: {}ms Variance: {:.2} ║\n",
perf.mean_frame_ms, perf.max_frame_ms, perf.variance
));
output.push_str("║ ║\n");
output.push_str("║ Test Results: ║\n");
let passed = report.total_passed;
let failed = report.total_failed;
output.push_str(&format!(
"║ ✓ Passed: {:<6} ✗ Failed: {:<6} ║\n",
passed, failed
));
output.push_str("║ ║\n");
let status = if perf.passed { "PASS" } else { "FAIL" };
output.push_str(&format!(
"║ Anomalies: {} Regressions: {} Status: {:<4} ║\n",
state.anomaly_count, state.regression_count, status
));
output.push_str("╠══════════════════════════════════════════════════════════════╣\n");
output.push_str("║ [q] Quit [p] Pause [r] Reset [s] Save Report ║\n");
output.push_str("╚══════════════════════════════════════════════════════════════╝\n");
output
}
#[must_use]
pub fn progress_bar(current: u32, total: u32, width: usize) -> String {
if total == 0 {
return format!("[{}]", " ".repeat(width));
}
let progress = (current as f64 / total as f64).min(1.0);
#[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
let filled = (progress * width as f64).round() as usize;
let empty = width - filled;
format!("[{}{}]", "█".repeat(filled), "░".repeat(empty))
}
#[cfg(feature = "tui-monitor")]
pub mod interactive {
#[allow(clippy::wildcard_imports)]
use super::*;
pub fn run_interactive(
_config: TuiConfig,
_state: &mut TuiState,
) -> Result<(), Box<dyn std::error::Error>> {
Err("Interactive TUI requires tui-monitor feature with ratatui".into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tui_config_default() {
let config = TuiConfig::default();
assert_eq!(config.refresh_rate_ms, 100);
assert!(config.show_frame_times);
assert!(config.show_memory_usage);
assert!(config.show_anomaly_alerts);
}
#[test]
fn test_tui_state_new() {
let state = TuiState::new(100);
assert_eq!(state.total_cycles, 100);
assert!(state.running);
assert!(!state.paused);
}
#[test]
fn test_format_memory() {
let mut state = TuiState::default();
state.memory_bytes = 512;
assert_eq!(state.format_memory(), "512 B");
state.memory_bytes = 2048;
assert_eq!(state.format_memory(), "2.0 KB");
state.memory_bytes = 5 * 1024 * 1024;
assert_eq!(state.format_memory(), "5.0 MB");
}
#[test]
fn test_sparkline_data() {
let mut state = TuiState::default();
state.frame_times = vec![10, 20, 30, 40, 50];
let data = state.sparkline_data();
assert_eq!(data.len(), 5);
assert_eq!(data[0], 0); assert_eq!(data[4], 7); }
#[test]
fn test_sparkline_string() {
let mut state = TuiState::default();
state.frame_times = vec![10, 20, 30, 40, 50];
let sparkline = state.sparkline_string();
assert_eq!(sparkline.chars().count(), 5);
assert!(sparkline.starts_with('▁'));
assert!(sparkline.ends_with('█'));
}
#[test]
fn test_progress_bar() {
assert_eq!(progress_bar(0, 100, 10), "[░░░░░░░░░░]");
assert_eq!(progress_bar(50, 100, 10), "[█████░░░░░]");
assert_eq!(progress_bar(100, 100, 10), "[██████████]");
assert_eq!(progress_bar(0, 0, 10), "[ ]"); }
#[test]
fn test_render_to_string() {
let state = TuiState::new(100);
let report = StressReport::default();
let perf = PerformanceResult {
passed: true,
max_frame_ms: 50,
mean_frame_ms: 40.0,
variance: 0.1,
pass_rate: 1.0,
violations: vec![],
};
let output = render_to_string(&state, &report, &perf);
assert!(output.contains("trueno-gpu Stress Test Monitor"));
assert!(output.contains("Cycle: 0/100"));
assert!(output.contains("PASS"));
}
#[test]
fn test_update_from_report() {
use super::super::stress::{FrameProfile, StressReport};
let mut state = TuiState::new(10);
let mut report = StressReport::default();
for i in 0..5 {
report.add_frame(FrameProfile {
cycle: i,
duration_ms: 50 + i as u64 * 10,
memory_bytes: 1024,
tests_passed: 5,
tests_failed: 0,
..Default::default()
});
}
state.update_from_report(&report);
assert_eq!(state.current_cycle, 5);
assert_eq!(state.frame_times.len(), 5);
assert!(state.current_fps > 0.0);
assert!((state.pass_rate - 1.0).abs() < 0.001);
}
}