use arrayvec::ArrayString;
use ratatui::style::Color;
use super::constants::{BACKEND_COLORS, throughput};
use super::dashboard::BackendView;
use super::types::{BackendChartData, ChartDataVec, ChartPoint, ChartX, ChartY, PointVec};
use crate::tui::app::ThroughputPoint;
const SPARKLINE_WIDTH: usize = 15;
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
#[cfg(test)]
#[must_use]
pub fn create_sparkline(value: u64, max_value: u64) -> String {
create_sparkline_text(value, max_value).to_string()
}
#[must_use]
pub fn create_sparkline_text(value: u64, max_value: u64) -> ArrayString<64> {
let filled = if max_value > 0 {
((value as f64 / max_value as f64) * SPARKLINE_WIDTH as f64) as usize
} else {
0
};
let filled = filled.min(SPARKLINE_WIDTH);
let empty = SPARKLINE_WIDTH.saturating_sub(filled);
let mut bar = ArrayString::<64>::new();
for _ in 0..filled {
bar.push_str("█");
}
for _ in 0..empty {
bar.push_str("░");
}
bar
}
pub fn build_chart_data(backend_views: &[BackendView]) -> (ChartDataVec, f64) {
#[inline]
fn extract_point_data(idx: usize, point: &ThroughputPoint) -> (ChartPoint, ChartPoint, ChartY) {
let x = ChartX::from(idx);
let sent = ChartY::from(point.sent_per_sec().get());
let recv = ChartY::from(point.received_per_sec().get());
(
ChartPoint::new(x, sent),
ChartPoint::new(x, recv),
sent.max(recv),
)
}
#[inline]
fn accumulate_points(
((mut sent_vec, mut recv_vec), max): ((PointVec, PointVec), ChartY),
(sent_point, recv_point, point_max): (ChartPoint, ChartPoint, ChartY),
) -> ((PointVec, PointVec), ChartY) {
sent_vec.push(sent_point);
recv_vec.push(recv_point);
((sent_vec, recv_vec), max.max(point_max))
}
backend_views
.iter()
.enumerate()
.map(|(index, backend)| {
let ((sent_points, recv_points), backend_max) = backend
.history
.iter()
.enumerate()
.map(|(idx, point)| extract_point_data(idx, point))
.fold(
((PointVec::new(), PointVec::new()), ChartY::from(0.0)),
accumulate_points,
);
(
BackendChartData::new(
backend.server.name.as_str().to_string(),
backend_color(index),
&sent_points,
&recv_points,
),
backend_max.get(),
)
})
.fold(
(ChartDataVec::new(), 0.0_f64),
|(mut chart_data, global_max), (backend_data, backend_max)| {
chart_data.push(backend_data);
(chart_data, global_max.max(backend_max))
},
)
}
#[inline]
#[must_use]
pub fn backend_color(index: usize) -> Color {
BACKEND_COLORS[index % BACKEND_COLORS.len()]
}
#[must_use]
pub fn round_up_throughput(value: f64) -> f64 {
if value == throughput::HUNDRED_MIB
|| value == throughput::TEN_MIB
|| value == throughput::ONE_MIB
{
value
} else if value > throughput::HUNDRED_MIB {
(value / throughput::TEN_MIB).ceil() * throughput::TEN_MIB
} else if value > throughput::TEN_MIB {
(value / throughput::ONE_MIB).ceil() * throughput::ONE_MIB
} else if value > throughput::ONE_MIB {
(value / throughput::HUNDRED_KIB).ceil() * throughput::HUNDRED_KIB
} else {
(value / throughput::TEN_KIB).ceil() * throughput::TEN_KIB
}
}
#[must_use]
pub fn format_throughput_label(value: f64) -> String {
if value >= throughput::ONE_MIB {
format!("{:.0} MiB/s", value / throughput::ONE_MIB)
} else if value >= throughput::ONE_KIB {
format!("{:.0} KiB/s", value / throughput::ONE_KIB)
} else {
format!("{value:.0} B/s")
}
}
#[must_use]
pub fn format_summary_throughput(latest_throughput: Option<&ThroughputPoint>) -> (String, String) {
use super::constants::text;
latest_throughput.map_or_else(
|| {
(
format!("{}{}", text::ARROW_UP, text::DEFAULT_THROUGHPUT),
format!("{}{}", text::ARROW_DOWN, text::DEFAULT_THROUGHPUT),
)
},
|point| {
(
format!("{}{}", text::ARROW_UP, point.sent_per_sec()),
format!("{}{}", text::ARROW_DOWN, point.received_per_sec()),
)
},
)
}
#[must_use]
pub const fn health_indicator(
status: crate::metrics::BackendHealthStatus,
) -> (&'static str, Color) {
use crate::metrics::BackendHealthStatus;
match status {
BackendHealthStatus::Healthy => ("●", Color::Green),
BackendHealthStatus::Degraded => ("◐", Color::Yellow),
BackendHealthStatus::Down => ("○", Color::Red),
}
}
#[cfg(test)]
#[must_use]
pub fn format_error_rate(rate: f64) -> String {
match rate {
r if r > 5.0 => format!(" ⚠ {r:.1}%"),
r if r > 0.0 => format!(" {r:.1}%"),
_ => String::new(),
}
}
#[must_use]
pub const fn error_rate_color(rate: f64) -> Color {
if rate > 5.0 {
Color::Red
} else {
Color::Yellow
}
}
#[must_use]
pub const fn error_count_color(has_errors: bool) -> Color {
use super::constants::styles;
if has_errors {
Color::Yellow
} else {
styles::VALUE_NEUTRAL
}
}
#[must_use]
pub const fn connection_failure_color(failures: u64) -> Color {
use super::constants::styles;
if failures > 0 {
Color::Red
} else {
styles::VALUE_NEUTRAL
}
}
#[must_use]
pub fn calculate_chart_bounds(max_throughput: f64) -> f64 {
use super::constants::chart;
let clamped = max_throughput.max(chart::MIN_THROUGHPUT);
round_up_throughput(clamped)
}
#[cfg(test)]
#[allow(clippy::float_cmp)] mod tests {
use super::*;
#[test]
fn test_backend_color_cycles() {
let color0 = backend_color(0);
let color_wrap = backend_color(BACKEND_COLORS.len());
assert_eq!(color0, color_wrap, "Should wrap around");
}
#[test]
fn test_backend_color_distinct() {
let color0 = backend_color(0);
let color1 = backend_color(1);
let color2 = backend_color(2);
assert_ne!(color0, color1);
assert_ne!(color1, color2);
assert_ne!(color0, color2);
}
#[test]
fn test_round_up_throughput() {
assert_eq!(round_up_throughput(110_000_000.0), 115_343_360.0);
assert_eq!(round_up_throughput(15_500_000.0), 15_728_640.0);
assert_eq!(round_up_throughput(1_250_000.0), 1_331_200.0);
assert_eq!(round_up_throughput(55_000.0), 61_440.0);
}
#[test]
fn test_round_up_throughput_exact_boundaries() {
assert_eq!(round_up_throughput(104_857_600.0), 104_857_600.0);
assert_eq!(round_up_throughput(10_485_760.0), 10_485_760.0);
assert_eq!(round_up_throughput(1_048_576.0), 1_048_576.0);
}
#[test]
fn test_format_throughput_label() {
assert_eq!(format_throughput_label(10_485_760.0), "10 MiB/s");
assert_eq!(format_throughput_label(500_000.0), "488 KiB/s");
assert_eq!(format_throughput_label(100.0), "100 B/s");
}
#[test]
fn test_format_throughput_label_boundaries() {
assert_eq!(format_throughput_label(1_048_576.0), "1 MiB/s");
assert_eq!(format_throughput_label(1_024.0), "1 KiB/s");
assert_eq!(format_throughput_label(0.0), "0 B/s");
}
#[test]
fn test_point_vec_type() {
let mut points = PointVec::new();
for i in 0..60 {
points.push(ChartPoint::new(
ChartX::from(i),
ChartY::from(f64::from(
u32::try_from(i * 1000).expect("test value fits into u32"),
)),
));
}
assert_eq!(points.len(), 60);
assert_eq!(points[0].x.get(), 0.0);
assert_eq!(points[59].x.get(), 59.0);
}
#[test]
fn test_chart_data_vec_type() {
let mut chart_data = ChartDataVec::new();
for i in 0..8 {
chart_data.push(BackendChartData::new(
format!("Server {i}"),
backend_color(i),
&PointVec::new(),
&PointVec::new(),
));
}
assert_eq!(chart_data.len(), 8);
assert_eq!(chart_data[0].name, "Server 0");
assert_eq!(chart_data[7].name, "Server 7");
}
#[test]
fn test_backend_chart_data_structure() {
let data = BackendChartData::new(
"Test Server".to_string(),
backend_color(0),
&PointVec::new(),
&PointVec::new(),
);
assert_eq!(data.name, "Test Server");
assert_eq!(data.sent_points_as_tuples().len(), 0);
assert_eq!(data.recv_points_as_tuples().len(), 0);
}
#[test]
fn test_format_summary_throughput_none() {
use super::super::constants::text;
let (up, down) = format_summary_throughput(None);
assert!(up.starts_with(text::ARROW_UP));
assert!(down.starts_with(text::ARROW_DOWN));
assert!(up.contains(text::DEFAULT_THROUGHPUT));
assert!(down.contains(text::DEFAULT_THROUGHPUT));
}
#[test]
fn test_format_summary_throughput_with_data() {
use super::super::constants::text;
use crate::types::tui::{CommandsPerSecond, Throughput, Timestamp};
let point = ThroughputPoint::new_backend(
Timestamp::now(),
Throughput::new(1_000_000.0),
Throughput::new(2_000_000.0),
CommandsPerSecond::new(10.0),
);
let (up, down) = format_summary_throughput(Some(&point));
assert!(up.starts_with(text::ARROW_UP));
assert!(down.starts_with(text::ARROW_DOWN));
assert!(up.contains("MiB/s") || up.contains("KiB/s") || up.contains("B/s"));
assert!(down.contains("MiB/s") || down.contains("KiB/s") || down.contains("B/s"));
}
#[test]
fn test_create_sparkline_full() {
let bar = create_sparkline(100, 100);
assert_eq!(bar.len(), SPARKLINE_WIDTH * "█".len());
assert_eq!(bar, "█".repeat(SPARKLINE_WIDTH));
}
#[test]
fn test_create_sparkline_empty() {
let bar = create_sparkline(0, 100);
assert_eq!(bar.len(), SPARKLINE_WIDTH * "░".len());
assert_eq!(bar, "░".repeat(SPARKLINE_WIDTH));
}
#[test]
fn test_create_sparkline_half() {
let bar = create_sparkline(50, 100);
let expected_filled = SPARKLINE_WIDTH / 2;
assert!(bar.starts_with(&"█".repeat(expected_filled)));
assert!(bar.ends_with(&"░".repeat(SPARKLINE_WIDTH - expected_filled)));
}
#[test]
fn test_create_sparkline_zero_max() {
let bar = create_sparkline(50, 0);
assert_eq!(bar, "░".repeat(SPARKLINE_WIDTH));
}
#[test]
fn create_sparkline_text_matches_owned_sparkline() {
for (value, max_value) in [(0, 100), (50, 100), (100, 100), (50, 0)] {
assert_eq!(
create_sparkline_text(value, max_value).as_str(),
create_sparkline(value, max_value)
);
}
}
#[test]
fn test_calculate_chart_bounds_below_min() {
use super::super::constants::chart;
let bounds = calculate_chart_bounds(500_000.0);
assert!(bounds >= chart::MIN_THROUGHPUT);
}
#[test]
fn test_calculate_chart_bounds_above_min() {
let bounds = calculate_chart_bounds(15_500_000.0);
assert_eq!(bounds, 15_728_640.0); }
#[test]
fn test_calculate_chart_bounds_zero() {
use super::super::constants::chart;
let bounds = calculate_chart_bounds(0.0);
assert_eq!(bounds, round_up_throughput(chart::MIN_THROUGHPUT));
}
}