use super::*;
use crate::cli::analytics::dashboard::{DashboardConfig, DashboardManager, HealthStatus};
use crate::cli::analytics::test_utils::{AnalyticsTestFixture, TestDataSet};
use crate::cli::analytics::{AnalyticsEngine, DashboardData};
use crate::cli::cost::{CostEntry, CostTracker};
use crate::cli::error::Result;
use crate::cli::history::{HistoryEntry, HistoryStore};
use chrono::{DateTime, Duration, Utc};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::RwLock;
pub struct DashboardTestFixture {
pub analytics_engine: AnalyticsEngine,
pub dashboard_manager: DashboardManager,
pub cost_tracker: Arc<RwLock<CostTracker>>,
pub history_store: Arc<RwLock<HistoryStore>>,
pub temp_dir: TempDir,
}
impl DashboardTestFixture {
pub async fn new() -> Result<Self> {
let base_fixture = AnalyticsTestFixture::new().await?;
let analytics_engine = AnalyticsEngine::new(
Arc::clone(&base_fixture.cost_tracker),
Arc::clone(&base_fixture.history_store),
Default::default(),
);
let dashboard_manager =
DashboardManager::new(analytics_engine.clone(), DashboardConfig::default());
Ok(Self {
analytics_engine,
dashboard_manager,
cost_tracker: base_fixture.cost_tracker,
history_store: base_fixture.history_store,
temp_dir: base_fixture.temp_dir,
})
}
pub async fn populate_test_data(&self, days: u32) -> Result<()> {
let generator = DashboardTestDataGenerator::new();
let test_data = generator.generate_dashboard_data(days);
self.load_data(&test_data).await
}
pub async fn load_data(&self, data: &TestDataSet) -> Result<()> {
let mut cost_tracker = self.cost_tracker.write().await;
let mut history_store = self.history_store.write().await;
for entry in &data.cost_entries {
cost_tracker.record_cost(entry.clone()).await?;
}
for entry in &data.history_entries {
history_store.store_entry(entry.clone()).await?;
}
Ok(())
}
}
pub struct DashboardTestDataGenerator {
base_time: DateTime<Utc>,
}
impl DashboardTestDataGenerator {
pub fn new() -> Self {
Self {
base_time: Utc::now() - Duration::days(30),
}
}
pub fn generate_dashboard_data(&self, days: u32) -> TestDataSet {
let mut data = TestDataSet {
sessions: Vec::new(),
cost_entries: Vec::new(),
history_entries: Vec::new(),
start_time: Utc::now() - Duration::days(days as i64),
end_time: Utc::now(),
};
for day in 0..days {
let day_offset = Duration::days(day as i64);
let day_start = Utc::now() - Duration::days((days - day) as i64);
for hour in 0..24 {
let timestamp = day_start + Duration::hours(hour);
let commands_this_hour = ((hour as f64 * 0.5).sin().abs() * 10.0) as usize + 1;
for cmd_idx in 0..commands_this_hour {
let session_id = uuid::Uuid::new_v4();
let command_name = format!("cmd_{}_{}", day, cmd_idx);
let cost = 0.01 + (cmd_idx as f64 * 0.001);
let mut cost_entry = CostEntry::new(
session_id,
command_name.clone(),
cost,
100 + cmd_idx as u32 * 10,
200 + cmd_idx as u32 * 20,
1000 + cmd_idx as u64 * 100,
"claude-3-opus".to_string(),
);
cost_entry.timestamp = timestamp;
data.cost_entries.push(cost_entry);
let mut history_entry = HistoryEntry::new(
session_id,
command_name,
vec![],
"output".to_string(),
cmd_idx % 10 != 9, 1000 + cmd_idx as u64 * 100,
);
history_entry.timestamp = timestamp;
data.history_entries.push(history_entry);
}
}
}
data
}
pub fn generate_time_series_data(&self, hours: u32) -> Vec<(DateTime<Utc>, f64)> {
let mut series = Vec::new();
let now = Utc::now();
for hour in 0..hours {
let timestamp = now - Duration::hours(hour as i64);
let value = ((hour as f64 * 0.1).sin().abs() * 100.0) + 50.0;
series.push((timestamp, value));
}
series.reverse();
series
}
pub fn generate_widget_data(&self, widget_type: &WidgetType) -> HashMap<String, Value> {
let mut data = HashMap::new();
match widget_type {
WidgetType::CostSummary => {
data.insert("total_cost".to_string(), serde_json::json!(125.43));
data.insert("today_cost".to_string(), serde_json::json!(12.34));
data.insert("trend".to_string(), serde_json::json!("+15%"));
}
WidgetType::CommandActivity => {
data.insert("total_commands".to_string(), serde_json::json!(1543));
data.insert("active_sessions".to_string(), serde_json::json!(5));
data.insert("commands_per_hour".to_string(), serde_json::json!(64.3));
}
WidgetType::SuccessRate => {
data.insert("success_rate".to_string(), serde_json::json!(94.5));
data.insert("failed_commands".to_string(), serde_json::json!(85));
data.insert("total_commands".to_string(), serde_json::json!(1543));
}
WidgetType::ResponseTime => {
data.insert("avg_response_ms".to_string(), serde_json::json!(850));
data.insert("p50_ms".to_string(), serde_json::json!(650));
data.insert("p95_ms".to_string(), serde_json::json!(1850));
data.insert("p99_ms".to_string(), serde_json::json!(3200));
}
}
data
}
}
#[derive(Debug, Clone)]
pub enum WidgetType {
CostSummary,
CommandActivity,
SuccessRate,
ResponseTime,
}
pub mod performance {
use super::*;
use std::time::Instant;
pub async fn measure_dashboard_generation(
fixture: &DashboardTestFixture,
iterations: usize,
) -> Vec<u128> {
let mut durations = Vec::new();
for _ in 0..iterations {
let start = Instant::now();
let _ = fixture.dashboard_manager.generate_live_data().await;
durations.push(start.elapsed().as_micros());
}
durations
}
}
pub mod assertions {
use super::*;
pub fn assert_dashboard_data_valid(data: &DashboardData) {
assert!(
data.today_cost >= 0.0,
"Today's cost should be non-negative"
);
assert!(
data.today_commands >= 0,
"Today's commands should be non-negative"
);
assert!(
data.success_rate >= 0.0 && data.success_rate <= 100.0,
"Success rate should be between 0 and 100"
);
}
}
pub mod perf {
use std::time::Instant;
pub struct Timer {
name: String,
start: Instant,
}
impl Timer {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
start: Instant::now(),
}
}
pub fn elapsed_ms(&self) -> u128 {
self.start.elapsed().as_millis()
}
pub fn report(&self) -> String {
format!("{}: {}ms", self.name, self.elapsed_ms())
}
pub fn name(&self) -> &str {
&self.name
}
}
pub struct PerfCollector {
measurements: Vec<(String, u128)>,
}
impl PerfCollector {
pub fn new() -> Self {
Self {
measurements: Vec::new(),
}
}
pub fn record(&mut self, name: &str, duration_ms: u128) {
self.measurements.push((name.to_string(), duration_ms));
}
pub fn summary(&self) -> PerfSummary {
let total: u128 = self.measurements.iter().map(|(_, d)| d).sum();
let count = self.measurements.len();
let average_ms = if count > 0 { total / count as u128 } else { 0 };
let max_ms = self.measurements.iter().map(|(_, d)| *d).max().unwrap_or(0);
let min_ms = self.measurements.iter().map(|(_, d)| *d).min().unwrap_or(0);
PerfSummary {
total_ms: total,
average_ms,
max_ms,
min_ms,
count,
}
}
}
#[derive(Debug)]
pub struct PerfSummary {
pub total_ms: u128,
pub average_ms: u128,
pub max_ms: u128,
pub min_ms: u128,
pub count: usize,
}
}