diff --git a/README.md b/README.md index e1d47d5..a1a6f2c 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ see for $name { + fn from(value: &str) -> Self { + Self(value.to_owned()) + } + } + impl$name { pub fn get(&self) -> &str { self.0.as_str() @@ -93,7 +100,7 @@ macro_rules! unit_struct { unit_struct!(ContainerName); unit_struct!(ContainerImage); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct StatefulList { pub state: ListState, pub items: Vec, @@ -154,7 +161,7 @@ impl StatefulList { .state .selected() .map_or(0, |value| if len > 0 { value + 1 } else { value }); - format!("{c}/{}", self.items.len()) + format!(" {c}/{}", self.items.len()) } } } @@ -234,7 +241,7 @@ impl fmt::Display for State { } /// Items for the container control list -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DockerControls { Pause, Restart, @@ -416,7 +423,7 @@ impl fmt::Display for LogsTz { /// Store the logs alongside a HashSet, each log *should* generate a unique timestamp, /// so if we store the timestamp separately in a HashSet, we can then check if we should insert a log line into the /// stateful list dependent on whethere the timestamp is in the HashSet or not -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Logs { logs: StatefulList>, tz: HashSet, @@ -475,7 +482,7 @@ impl Logs { } /// Info for each container -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ContainerItem { pub created: u64, pub cpu_stats: VecDeque, @@ -594,7 +601,7 @@ impl ContainerItem { } /// Container information panel headings + widths, for nice pretty formatting -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Columns { pub name: (Header, u8), pub state: (Header, u8), @@ -623,3 +630,98 @@ impl Columns { } } } + +#[cfg(test)] +mod tests { + use ratatui::widgets::ListItem; + + use crate::{ + app_data::{ContainerImage, Logs}, + ui::log_sanitizer, + }; + + use super::{ByteStats, ContainerName, CpuStats, LogsTz}; + + #[test] + // Display CpuStats as a string + fn test_container_state_cpustats_to_string() { + let test = |f: f64, s: &str| { + assert_eq!(CpuStats::new(f).to_string(), s); + }; + + test(0.0, "00.00%"); + test(1.5, "01.50%"); + test(15.15, "15.15%"); + test(150.15, "150.15%"); + } + + #[test] + // Display bytestats as a string, convert into correct data unit (Kb, MB, GB) + fn test_container_state_bytestats_to_string() { + let test = |u: u64, s: &str| { + assert_eq!(ByteStats::new(u).to_string(), s); + }; + + test(0, "0.00 kB"); + test(150, "0.15 kB"); + test(1500, "1.50 kB"); + test(150_000, "150.00 kB"); + test(1_500_000, "1.50 MB"); + test(15_000_000, "15.00 MB"); + test(150_000_000, "150.00 MB"); + test(1_500_000_000, "1.50 GB"); + test(15_000_000_000, "15.00 GB"); + test(150_000_000_000, "150.00 GB"); + } + + #[test] + /// ContainerName as string truncated correctly + fn test_container_state_container_name_to_string() { + let result = ContainerName::from("name_01"); + assert_eq!(result.to_string(), "name_01"); + + let result = ContainerName::from("name_01_name_01_name_01_name_01_"); + assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…"); + + let result = result.get(); + assert_eq!(result, "name_01_name_01_name_01_name_01_"); + } + + #[test] + /// ContainerImage as string truncated correctly + fn test_container_state_container_image() { + let result = ContainerImage::from("name_01"); + assert_eq!(result.to_string(), "name_01"); + + let result = ContainerImage::from("name_01_name_01_name_01_name_01_"); + assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…"); + + let result = result.get(); + assert_eq!(result, "name_01_name_01_name_01_name_01_"); + } + + #[test] + /// Logs can only contain 1 entry per LogzTz + fn test_container_state_logz() { + let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet"; + let tz = LogsTz::from(input); + let mut logs = Logs::default(); + let line = log_sanitizer::remove_ansi(input); + + logs.insert(ListItem::new(line.clone()), tz.clone()); + logs.insert(ListItem::new(line.clone()), tz.clone()); + logs.insert(ListItem::new(line), tz); + + assert_eq!(logs.logs.items.len(), 1); + + let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet"; + let tz = LogsTz::from(input); + let line = log_sanitizer::remove_ansi(input); + + logs.insert(ListItem::new(line.clone()), tz.clone()); + logs.insert(ListItem::new(line.clone()), tz.clone()); + logs.insert(ListItem::new(line), tz); + + assert_eq!(logs.logs.items.len(), 2); + } +} diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index a9fcadc..feb88cc 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -17,27 +17,6 @@ use crate::{ }; pub use container_state::*; -#[cfg(not(debug_assertions))] -/// Global app_state, stored in an Arc -#[derive(Debug, Clone)] -pub struct AppData { - containers: StatefulList, - error: Option, - sorted_by: Option<(Header, SortedOrder)>, - pub args: CliArgs, -} - -#[cfg(debug_assertions)] -/// Global app_state, stored in an Arc -#[derive(Debug, Clone)] -pub struct AppData { - containers: StatefulList, - error: Option, - sorted_by: Option<(Header, SortedOrder)>, - debug_string: String, - pub args: CliArgs, -} - #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum SortedOrder { Asc, @@ -75,18 +54,47 @@ impl fmt::Display for Header { } } +/// Global app_state, stored in an Arc +#[derive(Debug, Clone)] +#[cfg(not(test))] +pub struct AppData { + containers: StatefulList, + error: Option, + sorted_by: Option<(Header, SortedOrder)>, + pub args: CliArgs, +} + +#[derive(Debug, Clone)] +#[cfg(test)] +pub struct AppData { + pub containers: StatefulList, + pub error: Option, + pub sorted_by: Option<(Header, SortedOrder)>, + pub args: CliArgs, +} + impl AppData { - #[cfg(debug_assertions)] - pub fn get_debug_string(&self) -> &str { - &self.debug_string + /// Generate a default app_state + pub fn default(args: CliArgs) -> Self { + Self { + args, + containers: StatefulList::new(vec![]), + error: None, + sorted_by: None, + } } - #[cfg(debug_assertions)] - #[allow(unused)] - pub fn push_debug_string(&mut self, x: &str) { - self.debug_string.push_str(x); + /// Current time as unix timestamp + #[allow(clippy::expect_used)] + fn get_systemtime() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("In our known reality, this error should never occur") + .as_secs() } + /// Container sort related methods + /// Change the sorted order, also set the selected container state to match new order fn set_sorted(&mut self, x: Option<(Header, SortedOrder)>) { self.sorted_by = x; @@ -99,40 +107,6 @@ impl AppData { })); } - /// Current time as unix timestamp - #[allow(clippy::expect_used)] - fn get_systemtime() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("In our known reality, this error should never occur") - .as_secs() - } - - /// Generate a default app_state - #[cfg(not(debug_assertions))] - pub fn default(args: CliArgs) -> Self { - Self { - args, - containers: StatefulList::new(vec![]), - error: None, - sorted_by: None, - } - } - - /// Generate a default app_state - #[cfg(debug_assertions)] - pub fn default(args: CliArgs) -> Self { - Self { - args, - containers: StatefulList::new(vec![]), - error: None, - sorted_by: None, - debug_string: String::new(), - } - } - - /// Container sort related methods - /// Remove the sorted header & order, and sort by default - created datetime pub fn reset_sorted(&mut self) { self.set_sorted(None); @@ -237,6 +211,11 @@ impl AppData { self.containers.items.len() } + /// Get all the ContainerItems + pub const fn get_container_items(&self) -> &Vec { + &self.containers.items + } + /// Get title for containers section pub fn container_title(&self) -> String { self.containers.get_state_title() @@ -262,9 +241,9 @@ impl AppData { self.containers.previous(); } - /// Get Container items - pub const fn get_container_items(&self) -> &Vec { - &self.containers.items + /// Get ListState of containers + pub fn get_container_state(&mut self) -> &mut ListState { + &mut self.containers.state } /// Get Option of the current selected container @@ -283,16 +262,37 @@ impl AppData { .and_then(|i| self.containers.items.get_mut(i)) } - /// Get ListState of containers - pub fn get_container_state(&mut self) -> &mut ListState { - &mut self.containers.state + /// return a mutable container by given id + fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { + self.containers.items.iter_mut().find(|i| &i.id == id) } + /// Get the ContainerName of by ID + pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option { + self.containers + .items + .iter_mut() + .find(|i| &i.id == id) + .map(|i| i.name.clone()) + } + + /// Find the id of the currently selected container. + /// If any containers on system, will always return a ContainerId + /// Only returns None when no containers found. + pub fn get_selected_container_id(&self) -> Option { + self.get_selected_container().map(|i| i.id.clone()) + } + + /// Get the Id and State for the currently selected container - used by the exec check method + pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> { + self.get_selected_container() + .map(|i| (i.id.clone(), i.state, i.name.get().to_owned())) + } /// Selected DockerCommand methods /// Get the current selected docker command /// So know which command to execute - pub fn selected_docker_command(&self) -> Option { + pub fn selected_docker_controls(&self) -> Option { self.get_selected_container().and_then(|i| { i.docker_controls.state.selected().and_then(|x| { i.docker_controls @@ -302,6 +302,35 @@ impl AppData { }) }) } + + /// Change selected choice of docker commands of selected container + pub fn docker_controls_next(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.next(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_controls_previous(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.previous(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_controls_start(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.start(); + } + } + + /// Change selected choice of docker commands of selected container + pub fn docker_controls_end(&mut self) { + if let Some(i) = self.get_mut_selected_container() { + i.docker_controls.end(); + } + } + /// Get mutable Option of the currently selected container DockerControls state pub fn get_control_state(&mut self) -> Option<&mut ListState> { self.get_mut_selected_container() @@ -314,34 +343,6 @@ impl AppData { .map(|i| &mut i.docker_controls.items) } - /// Change selected choice of docker commands of selected container - pub fn docker_command_next(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.next(); - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_command_previous(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.previous(); - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_command_start(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.start(); - } - } - - /// Change selected choice of docker commands of selected container - pub fn docker_command_end(&mut self) { - if let Some(i) = self.get_mut_selected_container() { - i.docker_controls.end(); - } - } - /// Logs related methods /// Get the title for log panel for selected container, will be either @@ -349,16 +350,16 @@ impl AppData { /// 2) "logs - container_name" when no logs found, again 32 chars max /// 3) "" no container currently selected - aka no containers on system pub fn get_log_title(&self) -> String { - self.get_selected_container().map_or_else(String::new, |y| { - let logs_len = y.logs.get_state_title(); - // let mut name = y.name.clone(); - // name.truncate(32); - if logs_len.is_empty() { - format!("- {} ", y.name) - } else { - format!("{logs_len} - {}", y.name.get()) - } - }) + self.get_selected_container() + .map_or_else(String::new, |ci| { + let logs_len = ci.logs.get_state_title(); + let prefix = if logs_len.is_empty() { + String::from(" ") + } else { + format!("{logs_len} ") + }; + format!("{}- {}", prefix, ci.name.get()) + }) } /// select next selected log line @@ -389,19 +390,6 @@ impl AppData { } } - /// Chart data related methods - - /// Get mutable Option of the currently selected container chart data - pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> { - self.containers - .state - .selected() - .and_then(|i| self.containers.items.get_mut(i)) - .map(|i| i.get_chart_data()) - } - - /// Logs related methods - /// Get mutable Vec of current containers logs pub fn get_logs(&mut self) -> Vec> { self.containers @@ -420,6 +408,17 @@ impl AppData { .map(|i| i.logs.state()) } + /// Chart data related methods + + /// Get mutable Option of the currently selected container chart data + pub fn get_chart_data(&mut self) -> Option<(CpuTuple, MemTuple)> { + self.containers + .state + .selected() + .and_then(|i| self.containers.items.get_mut(i)) + .map(|i| i.get_chart_data()) + } + /// Error related methods /// return single app_state error @@ -445,6 +444,12 @@ impl AppData { self.get_selected_container().map_or(false, |i| i.is_oxker) } + /// Check if selected container is oxker and also that oxker is being run in a container + pub fn is_oxker_in_container(&self) -> bool { + self.get_selected_container() + .map_or(false, |i| i.is_oxker && self.args.in_container) + } + /// Find the widths for the strings in the containers panel. /// So can display nicely and evenly pub fn get_width(&self) -> Columns { @@ -485,36 +490,9 @@ impl AppData { /// Update related methods - /// return a mutable container by given id - fn get_container_by_id(&mut self, id: &ContainerId) -> Option<&mut ContainerItem> { - self.containers.items.iter_mut().find(|i| &i.id == id) - } - - /// Get the ContainerName of by ID - pub fn get_container_name_by_id(&mut self, id: &ContainerId) -> Option { - self.containers - .items - .iter_mut() - .find(|i| &i.id == id) - .map(|i| i.name.clone()) - } - - /// Find the id of the currently selected container. - /// If any containers on system, will always return a ContainerId - /// Only returns None when no containers found. - pub fn get_selected_container_id(&self) -> Option { - self.get_selected_container().map(|i| i.id.clone()) - } - - /// Get the Id and State for the currently selected container - used by the exec check method - pub fn get_selected_container_id_state_name(&self) -> Option<(ContainerId, State, String)> { - self.get_selected_container() - .map(|i| (i.id.clone(), i.state, i.name.get().to_owned())) - } - /// Update container mem, cpu, & network stats, in single function so only need to call .lock() once /// Will also, if a sort is set, sort the containers - pub fn update_stats( + pub fn update_stats_by_id( &mut self, id: &ContainerId, cpu_stat: Option, @@ -660,7 +638,6 @@ impl AppData { for mut i in logs { let tz = LogsTz::from(i.as_str()); - // Strip the timestamp if `-t` flag set if !timestamp { i = i.replace(&tz.to_string(), ""); } @@ -685,3 +662,1076 @@ impl AppData { } } } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names)] +mod tests { + + use super::*; + use crate::tests::{gen_appdata, gen_container_summary, gen_containers}; + use std::collections::VecDeque; + + // ******* // + // Sort by // + // ******* // + + #[test] + /// Sort by header: name + fn test_app_data_set_sort_by_header_name() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + // descending + app_data.set_sorted(Some((Header::Name, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("1")); + + // ascending + app_data.set_sorted(Some((Header::Name, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: state + fn test_app_data_set_sort_by_header_state() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.state = State::Exited; + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.state = State::Running; + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.state = State::Paused; + } + + // descending + app_data.set_sorted(Some((Header::State, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("2")); + + // ascending + app_data.set_sorted(Some((Header::State, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("1")); + } + + #[test] + /// Sort by header: status + fn test_app_data_set_sort_by_header_status() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.status = "Exited (0) 10 minutes ago".to_owned(); + } + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.status = "Up 2 hours (Paused)".to_owned(); + } + + // Sort by status + // descending + app_data.set_sorted(Some((Header::Status, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("2")); + + // ascending + app_data.set_sorted(Some((Header::Status, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: cpu + fn test_app_data_set_sort_by_header_cpu() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.cpu_stats = VecDeque::from([CpuStats::new(10.1)]); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.cpu_stats = VecDeque::from([CpuStats::new(8.1)]); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.cpu_stats = VecDeque::from([CpuStats::new(20.3)]); + } + + // descending + app_data.set_sorted(Some((Header::Cpu, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("2")); + + // ascending + app_data.set_sorted(Some((Header::Cpu, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: memory + fn test_app_data_set_sort_by_header_mem() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.mem_stats = VecDeque::from([ByteStats::new(40)]); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.mem_stats = VecDeque::from([ByteStats::new(80)]); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.mem_stats = VecDeque::from([ByteStats::new(2)]); + } + + // descending + app_data.set_sorted(Some((Header::Memory, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("3")); + + // ascending + app_data.set_sorted(Some((Header::Memory, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("2")); + } + + #[test] + /// Sort by header: id + fn test_app_data_set_sort_by_header_id() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + // descending + app_data.set_sorted(Some((Header::Id, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("1")); + + // ascending + app_data.set_sorted(Some((Header::Id, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: image + fn test_app_data_set_sort_by_header_image() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + // descending + app_data.set_sorted(Some((Header::Image, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("1")); + + // ascending + app_data.set_sorted(Some((Header::Image, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// Sort by header: rx + fn test_app_data_set_sort_by_header_rx() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.rx = ByteStats::new(40); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.rx = ByteStats::new(80); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.rx = ByteStats::new(2); + } + + // descending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("3")); + + // ascending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("1")); + assert_eq!(c.id, ContainerId::from("2")); + } + + #[test] + /// Sort by header: tx + fn test_app_data_set_sort_by_header_tx() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.rx = ByteStats::new(400); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.rx = ByteStats::new(80); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.rx = ByteStats::new(83); + } + + // descending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("2")); + + // ascending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("1")); + } + + #[test] + /// Sort by header when selected headers match + fn test_app_data_set_sort_by_header_match() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + // descending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Desc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("3")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("1")); + + // ascending + app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + #[test] + /// reset sorted + fn test_app_data_reset_sorted() { + let (_ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result, &containers); + + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("1")) { + i.rx = ByteStats::new(400); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("2")) { + i.rx = ByteStats::new(80); + } + if let Some(i) = app_data.get_container_by_id(&ContainerId::from("3")) { + i.rx = ByteStats::new(83); + } + + app_data.set_sorted(Some((Header::Rx, SortedOrder::Asc))); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("2")); + assert_eq!(b.id, ContainerId::from("3")); + assert_eq!(c.id, ContainerId::from("1")); + + app_data.set_sorted(None); + let result = app_data.get_container_items(); + let (a, b, c) = (&result[0], &result[1], &result[2]); + assert_eq!(a.id, ContainerId::from("1")); + assert_eq!(b.id, ContainerId::from("2")); + assert_eq!(c.id, ContainerId::from("3")); + } + + // **************** // + // Container state // + // **************** // + + #[test] + /// Get len of current containers vec + fn test_app_data_get_container_len() { + let (_ids, containers) = gen_containers(); + let app_data = gen_appdata(&containers); + assert_eq!(app_data.get_container_len(), 3); + } + + #[test] + /// Select the first container + fn test_app_data_containers_start() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // No container selected + let result = app_data.get_container_state(); + assert_eq!(result.selected(), None); + assert_eq!(result.offset(), 0); + + // First container selected + app_data.containers_start(); + let result = app_data.get_container_state(); + assert_eq!(result.selected(), Some(0)); + assert_eq!(result.offset(), 0); + + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("1"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("1"), + State::Running, + "container_1".to_owned() + )) + ); + + // Calling previous when at start has no effect + app_data.containers_previous(); + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("1"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("1"), + State::Running, + "container_1".to_owned() + )) + ); + } + + #[test] + /// advance container list state by one + /// get get_selected_container_id() & get_selected_container_id_state_name() return valid Some data + fn test_app_data_containers_next() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // Advance list state by 1 + app_data.containers_start(); + app_data.containers_next(); + + let result = app_data.get_container_state(); + assert_eq!(result.selected(), Some(1)); + assert_eq!(result.offset(), 0); + + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("2"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("2"), + State::Running, + "container_2".to_owned() + )) + ); + } + + #[test] + /// advance container list state to the end + /// get get_selected_container_id() & get_selected_container_id_state_name() return valid Some data + fn test_app_data_containers_end() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + app_data.containers_end(); + let result = app_data.get_container_state(); + assert_eq!(result.selected(), Some(2)); + assert_eq!(result.offset(), 0); + + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("3"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("3"), + State::Running, + "container_3".to_owned() + )) + ); + + // Calling previous when at end has no effect + app_data.containers_next(); + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("3"))); + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("3"), + State::Running, + "container_3".to_owned() + )) + ); + } + + #[test] + /// go to previous container + fn test_app_data_containers_prev() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + app_data.containers_end(); + app_data.containers_previous(); + let result = app_data.get_container_state(); + assert_eq!(result.selected(), Some(1)); + assert_eq!(result.offset(), 0); + } + + #[test] + // Get the currently selected container + fn test_app_data_get_selected_container() { + let (_ids, mut containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_selected_container(); + assert_eq!(result, None); + + app_data.containers.start(); + app_data.containers.next(); + + let result = app_data.get_selected_container(); + assert_eq!(result, Some(&containers[1])); + + // As above, but now as mut + let result = app_data.get_mut_selected_container(); + assert_eq!(result, Some(&mut containers[1])); + } + + #[test] + // Get mut container by id + fn test_app_data_get_container_by_id() { + let (_ids, mut containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_by_id(&ContainerId::from("2")); + assert_eq!(result, Some(&mut containers[1])); + } + + #[test] + // Get just the containers name by id + fn test_app_data_get_container_name_by_id() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_name_by_id(&ContainerId::from("2")); + assert_eq!(result, Some(ContainerName::from("container_2"))); + } + + #[test] + // Get the id of the currently selected container + fn test_app_data_get_selected_container_id() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_end(); + + let result = app_data.get_selected_container_id(); + assert_eq!(result, Some(ContainerId::from("3"))); + } + + #[test] + fn test_app_data_get_selected_container_id_state_name() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_end(); + + let result = app_data.get_selected_container_id_state_name(); + assert_eq!( + result, + Some(( + ContainerId::from("3"), + State::Running, + "container_3".to_owned() + )) + ); + } + + // ************** // + // DockerControls // + // ************** // + + #[test] + /// Docker commands returned correctly + fn test_app_data_selected_docker_command() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // No commands when no container selected + let result = app_data.selected_docker_controls(); + assert!(result.is_none()); + + // Correct commands returned + app_data.containers_start(); + app_data.docker_controls_start(); + + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Pause)); + } + + #[test] + /// Docker command next works + fn test_app_data_selected_docker_command_next() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_start(); + app_data.docker_controls_start(); + app_data.docker_controls_next(); + + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Restart)); + } + + #[test] + /// Dockercommand end works, and next has no effect when at end + fn test_app_data_selected_docker_command_end() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_start(); + app_data.docker_controls_end(); + + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Delete)); + + // Next has no effect when at end + app_data.docker_controls_next(); + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Delete)); + } + + #[test] + /// Docker commands previous works, and has no effect when at start + fn test_app_data_selected_docker_command_previous() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + app_data.containers_start(); + app_data.docker_controls_end(); + app_data.docker_controls_previous(); + + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Stop)); + + // previous has no effect when at start + app_data.docker_controls_start(); + app_data.docker_controls_previous(); + let result = app_data.selected_docker_controls(); + assert_eq!(result, Some(DockerControls::Pause)); + } + + #[test] + /// DockerCommands get correct controls dependant on container state + fn test_app_data_get_control_items() { + let test_state = |state: State, expected: &mut Vec| { + let gen_item_state = |state: State| { + ContainerItem::new( + 1, + ContainerId::from("1"), + "image_1".to_owned(), + false, + "container_1".to_owned(), + state, + "Up 1 hour".to_owned(), + ) + }; + let mut app_data = gen_appdata(&vec![gen_item_state(state)]); + app_data.containers_start(); + app_data.docker_controls_start(); + + let result = app_data.get_control_items(); + assert_eq!(result, Some(expected)); + }; + + test_state( + State::Dead, + &mut vec![ + DockerControls::Start, + DockerControls::Restart, + DockerControls::Delete, + ], + ); + test_state( + State::Exited, + &mut vec![ + DockerControls::Start, + DockerControls::Restart, + DockerControls::Delete, + ], + ); + test_state( + State::Paused, + &mut vec![ + DockerControls::Unpause, + DockerControls::Stop, + DockerControls::Delete, + ], + ); + test_state(State::Removing, &mut vec![DockerControls::Delete]); + test_state( + State::Restarting, + &mut vec![DockerControls::Stop, DockerControls::Delete], + ); + test_state( + State::Running, + &mut vec![ + DockerControls::Pause, + DockerControls::Restart, + DockerControls::Stop, + DockerControls::Delete, + ], + ); + test_state(State::Unknown, &mut vec![DockerControls::Delete]); + } + + // **** // + // Logs // + // **** // + + #[test] + /// log title string generated correctly + fn test_app_data_get_log_title() { + let (ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + // No container selected select + let result = app_data.get_log_title(); + assert_eq!(result, ""); + + // No logs + app_data.containers.start(); + let result = app_data.get_log_title(); + assert_eq!(result, " - container_1"); + + // On last line of logs + let logs = (1..=3).map(|i| format!("{i}")).collect::>(); + app_data.update_log_by_id(logs, &ids[0]); + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + + // Change log state to no longer be at the end + app_data.log_previous(); + let result = app_data.get_log_title(); + assert_eq!(result, " 2/3 - container_1"); + } + + #[test] + /// log title string generated correctly after container change + fn test_app_data_get_log_title_after_container_change() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // No container selected select + let result = app_data.get_log_title(); + assert_eq!(result, ""); + + app_data.containers_start(); + + let result = app_data.get_log_title(); + assert_eq!(result, " - container_1"); + + // change container + app_data.containers_next(); + let result = app_data.get_log_title(); + assert_eq!(result, " - container_2"); + + // On last line of logs + let logs = (1..=3).map(|i| format!("{i}")).collect::>(); + app_data.update_log_by_id(logs, &ids[1]); + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_2"); + + // Change log state to no longer be at the end + app_data.log_previous(); + let result = app_data.get_log_title(); + assert_eq!(result, " 2/3 - container_2"); + } + + #[test] + /// update logs by id works + fn test_app_data_update_log_by_id() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + // No container selected select + let result = app_data.get_log_title(); + assert_eq!(result, ""); + + app_data.containers_start(); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + + app_data.update_log_by_id(logs, &ids[0]); + // app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_logs(); + assert_eq!(result.len(), 3); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + } + + #[test] + /// logs state reset to start + fn test_app_data_logs_start() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + app_data.containers_start(); + app_data.update_log_by_id(logs, &ids[0]); + + app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + } + + #[test] + /// logs state end goes to the end of the logs list + fn test_app_data_logs_end() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + app_data.containers_start(); + app_data.update_log_by_id(logs, &ids[0]); + + app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + + app_data.log_end(); + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + } + + #[test] + /// logs state next works + /// At end has no effect + fn test_app_data_logs_next() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + app_data.containers_start(); + app_data.update_log_by_id(logs, &ids[0]); + + app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + + app_data.log_next(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(1)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 2/3 - container_1"); + + app_data.log_next(); + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + app_data.log_next(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + } + + #[test] + /// logs state previous works + /// previous at start has no effect + fn test_app_data_logs_previous() { + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + app_data.containers_start(); + app_data.update_log_by_id(logs, &ids[0]); + + app_data.log_end(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(2)); + assert_eq!(result.unwrap().offset(), 0); + + let result = app_data.get_log_title(); + assert_eq!(result, " 3/3 - container_1"); + + app_data.log_previous(); + + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(1)); + assert_eq!(result.unwrap().offset(), 0); + let result = app_data.get_log_title(); + assert_eq!(result, " 2/3 - container_1"); + + app_data.log_previous(); + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + + app_data.log_previous(); + let result = app_data.get_log_state(); + assert!(result.is_some()); + assert_eq!(result.as_ref().unwrap().selected(), Some(0)); + assert_eq!(result.unwrap().offset(), 0); + let result = app_data.get_log_title(); + assert_eq!(result, " 1/3 - container_1"); + } + + // ********** // + // Chart data // + // ********** // + + #[test] + /// Chart data returned correctly + fn test_app_data_get_chart_data() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_chart_data(); + assert!(result.is_none()); + + app_data.containers_start(); + + if let Some(item) = app_data.get_container_by_id(&ContainerId::from("1")) { + item.cpu_stats = VecDeque::from([CpuStats::new(1.1), CpuStats::new(1.2)]); + item.mem_stats = VecDeque::from([ByteStats::new(1), ByteStats::new(2)]); + } + + let result = app_data.get_chart_data(); + assert_eq!( + result, + Some(( + ( + vec![(0.0, 1.1), (1.0, 1.2)], + CpuStats::new(1.2), + State::Running + ), + ( + vec![(0.0, 1.0), (1.0, 2.0)], + ByteStats::new(2), + State::Running + ) + )) + ); + } + + // ********** // + // Chart data // + // ********** // + + #[test] + /// Header widths return correctly + fn test_app_data_get_width() { + let (_ids, containers) = gen_containers(); + let app_data = gen_appdata(&containers); + + let result = app_data.get_width(); + let expected = Columns { + name: (Header::Name, 11), + state: (Header::State, 11), + status: (Header::Status, 16), + cpu: (Header::Cpu, 7), + mem: (Header::Memory, 7, 7), + id: (Header::Id, 8), + image: (Header::Image, 7), + net_rx: (Header::Rx, 7), + net_tx: (Header::Tx, 7), + }; + assert_eq!(result, expected); + } + + // ************** // + // Update mtehods // + // ************** // + + #[test] + /// Update stats functioning + fn test_app_data_update_stats() { + let (ids, containers) = gen_containers(); + + let mut app_data = gen_appdata(&containers); + + let result = app_data.get_container_items(); + assert_eq!(result[0], containers[0]); + + app_data.update_stats_by_id(&ids[0], Some(10.0), Some(10), 10, 10, 10); + + let result = app_data.get_container_items(); + assert_ne!(result[0], containers[0]); + assert_eq!(result[0].cpu_stats, VecDeque::from([CpuStats::new(10.0)])); + assert_eq!(result[0].mem_stats, VecDeque::from([ByteStats::new(10)])); + assert_eq!(result[0].mem_limit, ByteStats::new(10)); + assert_eq!(result[0].rx, ByteStats::new(10)); + assert_eq!(result[0].tx, ByteStats::new(10)); + } + + #[test] + /// Update stats functioning + fn test_app_data_update_containers() { + let (_ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + let result_pre = app_data.get_container_items().clone(); + let mut input = vec![ + gen_container_summary(1, "paused"), + gen_container_summary(2, "dead"), + ]; + + app_data.update_containers(&mut input); + let result_post = app_data.get_container_items(); + assert_ne!(&result_pre, result_post); + assert_eq!(result_post[0].state, State::Paused); + assert_eq!(result_post[1].state, State::Dead); + } + + #[test] + /// Update logs don't work if container is_oxker: true + fn test_app_data_update_log_by_id_is_oxker() { + let (ids, mut containers) = gen_containers(); + containers[0].is_oxker = true; + let mut app_data = gen_appdata(&containers); + let logs = (1..=3).map(|i| format!("{i} {i}")).collect::>(); + + app_data.update_log_by_id(logs, &ids[0]); + app_data.log_start(); + + let result = app_data.get_log_state(); + assert!(result.is_none()); + } +} diff --git a/src/docker_data/mod.rs b/src/docker_data/mod.rs index 8964ebf..caa5a18 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -71,6 +71,7 @@ pub struct DockerData { impl DockerData { /// Use docker stats to calculate current cpu usage #[allow(clippy::cast_precision_loss)] + // FIX: this can overflow fn calculate_usage(stats: &Stats) -> f64 { let mut cpu_percentage = 0.0; let previous_cpu = stats.precpu_stats.cpu_usage.total_usage; @@ -150,7 +151,7 @@ impl DockerData { app_data .lock() - .update_stats(&id, cpu_stats, mem_stat, mem_limit, rx, tx); + .update_stats_by_id(&id, cpu_stats, mem_stat, mem_limit, rx, tx); } } spawns.lock().remove(&spawn_id); @@ -162,7 +163,6 @@ impl DockerData { /// Update all stats, spawn each container into own tokio::spawn thread fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) { for (state, id) in all_ids { - // let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len()))); let docker = Arc::clone(&self.docker); let app_data = Arc::clone(&self.app_data); let spawns = Arc::clone(&self.spawns); @@ -436,7 +436,7 @@ impl DockerData { } /// Send an update message every x ms, where x is the args.docker_interval - fn croner(args: &CliArgs, docker_tx: Sender) { + fn scheduler(args: &CliArgs, docker_tx: Sender) { let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval)); let mut now = std::time::Instant::now(); tokio::spawn(async move { @@ -472,10 +472,152 @@ impl DockerData { spawns: Arc::new(Mutex::new(HashMap::new())), }; inner.initialise_container_data().await; - Self::croner(&args, docker_tx); + Self::scheduler(&args, docker_tx); inner.message_handler().await; } } } // tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers +#[cfg(test)] +mod tests { + use bollard::container::{ + BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData, + }; + + use super::*; + + #[allow(clippy::too_many_lines)] + fn gen_stats(x: u64, y: u64) -> Stats { + Stats { + read: String::new(), + preread: String::new(), + num_procs: 0, + pids_stats: PidsStats { + current: None, + limit: None, + }, + network: None, + networks: None, + memory_stats: MemoryStats { + stats: None, + max_usage: None, + usage: None, + failcnt: None, + limit: None, + commit: None, + commit_peak: None, + commitbytes: None, + commitpeakbytes: None, + privateworkingset: None, + }, + blkio_stats: BlkioStats { + io_service_bytes_recursive: None, + io_serviced_recursive: None, + io_queue_recursive: None, + io_service_time_recursive: None, + io_wait_time_recursive: None, + io_merged_recursive: None, + io_time_recursive: None, + sectors_recursive: None, + }, + cpu_stats: CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![ + 291_593_800, + 182_192_900, + 195_048_700, + 23_032_300, + 132_928_700, + 235_555_600, + 120_225_700, + 175_752_000, + 213_060_300, + 95_321_600, + 226_821_000, + 0, + 109_151_300, + 0, + 86_240_200, + 1_884_400, + 59_077_300, + 23_224_900, + 95_386_300, + 144_987_400, + ]), + total_usage: 250_000_000, + usage_in_usermode: 1_020_000_000, + usage_in_kernelmode: 1_030_000_000, + }, + system_cpu_usage: Some(x), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }, + precpu_stats: CPUStats { + cpu_usage: CPUUsage { + percpu_usage: Some(vec![ + 291_593_800, + 182_192_900, + 195_048_700, + 23_032_300, + 132_928_700, + 235_555_600, + 120_225_700, + 175_752_000, + 213_060_300, + 95_321_600, + 226_821_000, + 0, + 109_151_300, + 0, + 86_240_200, + 1_884_400, + 59_077_300, + 23_224_900, + 93_831_100, + 144_987_400, + ]), + total_usage: 200_000_000, + usage_in_usermode: 1_020_000_000, + usage_in_kernelmode: 1_020_000_000, + }, + system_cpu_usage: Some(y), + online_cpus: Some(1), + throttling_data: ThrottlingData { + periods: 0, + throttled_periods: 0, + throttled_time: 0, + }, + }, + storage_stats: StorageStats { + read_count_normalized: None, + read_size_bytes: None, + write_count_normalized: None, + write_size_bytes: None, + }, + name: "/container_1".to_owned(), + id: "1".to_owned(), + } + } + + #[test] + #[allow(clippy::float_cmp)] + /// Test the stats calculator, had to cheat here to get round input/outputs + fn test_calculate_usage_no_previous_cpu() { + let stats = gen_stats(1_000_000_000, 900_000_000); + let result = DockerData::calculate_usage(&stats); + assert_eq!(result, 50.0); + + let stats = gen_stats(1_000_000_000, 800_000_000); + let result = DockerData::calculate_usage(&stats); + assert_eq!(result, 25.0); + + let stats = gen_stats(1_000_000_000, 750_000_000); + let result = DockerData::calculate_usage(&stats); + assert_eq!(result, 20.00); + } +} diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 1f7fad0..ef630b9 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -268,11 +268,11 @@ impl InputHandler { // This isn't great, just means you can't send docker commands before full initialization of the program let panel = self.gui_state.lock().get_selected_panel(); if panel == SelectablePanel::Commands { - let option_command = self.app_data.lock().selected_docker_command(); + let option_command = self.app_data.lock().selected_docker_controls(); if let Some(command) = option_command { // Poor way of disallowing commands to be sent to a containerised okxer - if self.app_data.lock().is_oxker() { + if self.app_data.lock().is_oxker_in_container() { return; }; let option_id = self.app_data.lock().get_selected_container_id(); @@ -337,7 +337,7 @@ impl InputHandler { match selected_panel { SelectablePanel::Containers => locked_data.containers_start(), SelectablePanel::Logs => locked_data.log_start(), - SelectablePanel::Commands => locked_data.docker_command_start(), + SelectablePanel::Commands => locked_data.docker_controls_start(), } } @@ -348,7 +348,7 @@ impl InputHandler { match selected_panel { SelectablePanel::Containers => locked_data.containers_end(), SelectablePanel::Logs => locked_data.log_end(), - SelectablePanel::Commands => locked_data.docker_command_end(), + SelectablePanel::Commands => locked_data.docker_controls_end(), } } @@ -481,7 +481,7 @@ impl InputHandler { match selected_panel { SelectablePanel::Containers => locked_data.containers_next(), SelectablePanel::Logs => locked_data.log_next(), - SelectablePanel::Commands => locked_data.docker_command_next(), + SelectablePanel::Commands => locked_data.docker_controls_next(), }; } @@ -492,7 +492,7 @@ impl InputHandler { match selected_panel { SelectablePanel::Containers => locked_data.containers_previous(), SelectablePanel::Logs => locked_data.log_previous(), - SelectablePanel::Commands => locked_data.docker_command_previous(), + SelectablePanel::Commands => locked_data.docker_controls_previous(), } } } diff --git a/src/main.rs b/src/main.rs index d976325..a6d1b6d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -164,3 +164,82 @@ async fn main() { } } } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)] +mod tests { + use bollard::service::ContainerSummary; + + use crate::{ + app_data::{AppData, ContainerId, ContainerItem, State, StatefulList}, + parse_args::CliArgs, + }; + + pub const fn gen_args() -> CliArgs { + CliArgs { + color: false, + docker_interval: 1000, + gui: true, + host: None, + in_container: false, + save_dir: None, + raw: false, + show_self: false, + timestamp: false, + use_cli: false, + } + } + + pub fn gen_item(id: &ContainerId, index: usize) -> ContainerItem { + ContainerItem::new( + u64::try_from(index).unwrap(), + id.clone(), + format!("image_{index}"), + false, + format!("container_{index}"), + State::Running, + format!("Up {index} hour"), + ) + } + + pub fn gen_appdata(containers: &[ContainerItem]) -> AppData { + AppData { + containers: StatefulList::new(containers.to_vec()), + error: None, + sorted_by: None, + args: gen_args(), + } + } + + pub fn gen_containers() -> (Vec, Vec) { + let ids = (1..=3) + .map(|i| ContainerId::from(format!("{i}").as_str())) + .collect::>(); + let containers = ids + .iter() + .enumerate() + .map(|(index, id)| gen_item(id, index + 1)) + .collect::>(); + (ids, containers) + } + + pub fn gen_container_summary(index: usize, state: &str) -> ContainerSummary { + ContainerSummary { + id: Some(format!("{index}")), + names: Some(vec![format!("container_{}", index)]), + image: Some(format!("image_{index}")), + image_id: Some(format!("{index}")), + command: None, + created: Some(i64::try_from(index).unwrap()), + ports: None, + size_rw: None, + size_root_fs: None, + labels: None, + state: Some(state.to_owned()), + status: Some(format!("Up {index} hour")), + host_config: None, + network_settings: None, + mounts: None, + } + } +} diff --git a/src/ui/color_match.rs b/src/ui/color_match.rs index b35112c..a1171f2 100644 --- a/src/ui/color_match.rs +++ b/src/ui/color_match.rs @@ -72,3 +72,79 @@ pub mod log_sanitizer { } } } + +#[cfg(test)] +mod tests { + use ratatui::{ + style::{Color, Style}, + text::{Line, Span}, + }; + + use super::log_sanitizer; + + // This spells out "oxker", with each char having a foreground and background colour + const INPUT: &str = "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m"; + + #[test] + /// Return test raw, as in show escape codes + fn color_match_raw() { + let result = log_sanitizer::raw(INPUT); + let expected = vec![Line { + spans: [Span { + content: std::borrow::Cow::Borrowed( + "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m", + ), + style: Style::default(), + }] + .to_vec(), + alignment: None, + }]; + assert_eq!(result, expected); + } + + #[test] + // Use the escape codes to colorize the text + fn color_match_colorize() { + let result = log_sanitizer::colorize_logs(INPUT); + let expected = vec![Line { + spans: vec![ + Span { + content: std::borrow::Cow::Borrowed("o"), + style: Style::default().fg(Color::Red).bg(Color::White), + }, + Span { + content: std::borrow::Cow::Borrowed("x"), + style: Style::default().fg(Color::Green).bg(Color::Black), + }, + Span { + content: std::borrow::Cow::Borrowed("k"), + style: Style::default().fg(Color::Yellow).bg(Color::Red), + }, + Span { + content: std::borrow::Cow::Borrowed("e"), + style: Style::default().fg(Color::Blue).bg(Color::Green), + }, + Span { + content: std::borrow::Cow::Borrowed("r"), + style: Style::default().fg(Color::Magenta).bg(Color::Yellow), + }, + ], + alignment: None, + }]; + assert_eq!(result, expected); + } + + #[test] + // Remove all escape ansi codes from given input + fn color_match_remove_ansi() { + let result = log_sanitizer::remove_ansi(INPUT); + let expected = vec![Line { + spans: vec![Span { + content: std::borrow::Cow::Borrowed("oxker"), + style: Style::default(), + }], + alignment: None, + }]; + assert_eq!(result, expected); + } +} diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 708f197..7ab880e 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -66,10 +66,10 @@ fn generate_block<'a>( .update_region_map(Region::Panel(panel), area); let mut title = match panel { SelectablePanel::Containers => { - format!("{} {}", panel.title(), app_data.lock().container_title()) + format!("{}{}", panel.title(), app_data.lock().container_title()) } SelectablePanel::Logs => { - format!("{} {}", panel.title(), app_data.lock().get_log_title()) + format!("{}{}", panel.title(), app_data.lock().get_log_title()) } SelectablePanel::Commands => String::new(), }; @@ -95,6 +95,7 @@ pub fn commands( gui_state: &Arc>, ) { let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands); + // let block = block(); let items = app_data.lock().get_control_items().map_or(vec![], |i| { i.iter() .map(|c| { @@ -115,6 +116,7 @@ pub fn commands( if let Some(i) = app_data.lock().get_control_state() { f.render_stateful_widget(items, area, i); } else { + let block = || generate_block(app_data, area, fd, gui_state, SelectablePanel::Commands); let paragraph = Paragraph::new("") .block(block()) .alignment(Alignment::Center); @@ -207,7 +209,6 @@ pub fn containers( f: &mut Frame, fd: &FrameData, gui_state: &Arc>, - widths: &Columns, ) { let block = generate_block(app_data, area, fd, gui_state, SelectablePanel::Containers); @@ -215,7 +216,7 @@ pub fn containers( .lock() .get_container_items() .iter() - .map(|i| ListItem::new(format_containers(i, widths))) + .map(|i| ListItem::new(format_containers(i, &fd.columns))) .collect::>(); if items.is_empty() { @@ -734,7 +735,7 @@ pub fn help_box(f: &mut Frame) { .title(title) .borders(Borders::ALL) .border_type(BorderType::Rounded) - .border_style(Style::default().fg(Color::Black)); + .border_style(Style::default().fg(Color::Black).bg(Color::Magenta)); // Order is important here f.render_widget(Clear, area); @@ -759,7 +760,10 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co Span::from("Are you sure you want to delete container: "), Span::styled( name.to_string(), - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), + Style::default() + .fg(Color::Red) + .bg(Color::White) + .add_modifier(Modifier::BOLD), ), ]); @@ -776,19 +780,22 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co Block::default() .border_type(BorderType::Rounded) .borders(Borders::ALL) + .style(Style::default().bg(Color::White)) }; let yes_para = Paragraph::new(yes_text) .alignment(Alignment::Center) .block(button_block()); + // Need to add some padding for the borders - let yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9); + let _yes_chars = u16::try_from(yes_text.chars().count() + 2).unwrap_or(9); let no_para = Paragraph::new(no_text) .alignment(Alignment::Center) .block(button_block()); + // Need to add some padding for the borders - let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8); + // let no_chars = u16::try_from(no_text.chars().count() + 2).unwrap_or(8); let area = popup( lines, @@ -811,23 +818,15 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co ) .split(area); - // Should maybe have a differenet button_space IF the f.width() is within 2 chars of no_chars + yes_chars? - let button_spacing = (max_line_width - no_chars - yes_chars) / 3; - - let button_spacing = if (button_spacing + max_line_width) > f.size().width { - 1 - } else { - button_spacing - }; let split_buttons = Layout::default() .direction(Direction::Horizontal) .constraints( [ - Constraint::Max(button_spacing), - Constraint::Min(no_chars), - Constraint::Max(button_spacing), - Constraint::Min(yes_chars), - Constraint::Max(button_spacing), + Constraint::Percentage(10), + Constraint::Percentage(35), + Constraint::Percentage(10), + Constraint::Percentage(35), + Constraint::Percentage(10), ] .as_ref(), ) @@ -889,6 +888,8 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) { .alignment(Alignment::Center); let area = popup(lines, max_line_width, f.size(), BoxLocation::MiddleCentre); + + // let (paragraph, area) = gen_error(f, error, seconds); f.render_widget(Clear, area); f.render_widget(paragraph, area); } @@ -948,24 +949,1803 @@ fn popup(text_lines: usize, text_width: usize, r: Rect, box_location: BoxLocatio .split(popup_layout[indexes.0])[indexes.1] } -#[cfg(debug_assertions)] -// Single row at the top of the screen for debugging -pub fn debug_bar(area: Rect, f: &mut Frame, debug_string: &str) { - let block = Block::default().style(Style::default().bg(Color::Red)); - let paragraph = Paragraph::new(debug_string) - .style(Style::default().fg(Color::White)) - .block(block); - f.render_widget(paragraph, area); +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names)] +mod tests { + + use std::{ops::RangeInclusive, sync::Arc}; + + use parking_lot::Mutex; + use ratatui::{ + backend::TestBackend, + layout::Rect, + style::{Color, Modifier}, + Terminal, + }; + use uuid::Uuid; + + use crate::{ + app_data::{ + AppData, ContainerId, ContainerImage, ContainerName, Header, SortedOrder, State, + StatefulList, + }, + app_error::AppError, + tests::{gen_appdata, gen_container_summary, gen_containers}, + ui::{draw_frame, GuiState}, + }; + + use super::{FrameData, ORANGE, VERSION}; + + struct TuiTestSetup { + app_data: Arc>, + gui_state: Arc>, + fd: FrameData, + area: Rect, + terminal: Terminal, + ids: Vec, + } + + const BORDER_CHARS: [&str; 6] = ["╭", "╮", "─", "│", "╰", "╯"]; + + /// Generate state to be used in *most* gui tests + fn test_setup(w: u16, h: u16, control_start: bool, container_start: bool) -> TuiTestSetup { + let backend = TestBackend::new(w, h); + let terminal = Terminal::new(backend).unwrap(); + + let (ids, containers) = gen_containers(); + let mut app_data = gen_appdata(&containers); + if control_start { + app_data.docker_controls_start(); + } + if container_start { + app_data.containers_start(); + } + + let gui_state = GuiState::default(); + + let app_data = Arc::new(Mutex::new(app_data)); + let gui_state = Arc::new(Mutex::new(gui_state)); + + let fd = FrameData::from((app_data.lock(), gui_state.lock())); + let area = Rect::new(0, 0, w, h); + TuiTestSetup { + app_data, + gui_state, + fd, + area, + terminal, + ids, + } + } + + /// Insert some logs into the first container + fn insert_logs(setup: &TuiTestSetup) { + let logs = (1..=3).map(|i| format!("{i} line {i}")).collect::>(); + setup.app_data.lock().update_log_by_id(logs, &setup.ids[0]); + } + + // ******************** // + // DockerControls panel // + // ******************** // + + #[test] + // Test that when DockerCommands are available, they are drawn correctly, dependant on container state + fn test_draw_blocks_commands_none() { + let (w, h) = (12, 6); + let mut setup = test_setup(w, h, false, false); + + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let expected = [ + "╭──────────╮", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + + #[test] + // Test that when DockerCommands are available, they are drawn correctly, dependant on container state + fn test_draw_blocks_commands_some() { + let (w, h) = (12, 6); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let expected = [ + "╭──────────╮", + "│▶ pause │", + "│ restart │", + "│ stop │", + "│ delete │", + "╰──────────╯", + ]; + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // Check the text color is correct + match index { + // pause + 15..=19 => { + assert_eq!(result_cell.fg, Color::Yellow); + } + // restart + 27..=33 => { + assert_eq!(result_cell.fg, Color::Magenta); + } + // stop + 39..=42 => { + assert_eq!(result_cell.fg, Color::Red); + } + // delete + 51..=56 => { + assert_eq!(result_cell.fg, Color::Gray); + } + // no text + _ => { + assert_eq!(result_cell.fg, Color::Reset); + } + } + if result_cell.symbol().starts_with('▶') { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + + // Change the controls state + setup + .app_data + .lock() + .update_containers(&mut vec![gen_container_summary(1, "paused")]); + setup.app_data.lock().docker_controls_next(); + + let expected = [ + "╭──────────╮", + "│ resume │", + "│▶ stop │", + "│ delete │", + "│ │", + "╰──────────╯", + ]; + + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // Chceck the text color is correct + match index { + // resume + 15..=20 => { + assert_eq!(result_cell.fg, Color::Blue); + } + // stop + 27..=30 => { + assert_eq!(result_cell.fg, Color::Red); + } + // delete + 39..=44 => { + assert_eq!(result_cell.fg, Color::Gray); + } + // no text + _ => { + assert_eq!(result_cell.fg, Color::Reset); + } + } + if result_cell.symbol().starts_with('▶') { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + + #[test] + // When control panel is selected, the border is blue, if not then white, selected text is highlighted + fn test_draw_blocks_commands_panel_selected_color() { + let (w, h) = (12, 6); + let mut setup = test_setup(w, h, true, true); + let expected = [ + "╭──────────╮", + "│▶ pause │", + "│ restart │", + "│ stop │", + "│ delete │", + "╰──────────╯", + ]; + + // Unselected, has a grey border + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + + // Control panel now selected, should have a blue border + setup.gui_state.lock().next_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup + .terminal + .draw(|f| { + super::commands(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::LightCyan); + } + // Make sure that the selected line has bold text + match index { + // pause + 13..=22 => { + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _ => { + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + // *********************** // + // Container summary panel // + // *********************** // + + // Check that the correct solor is applied to the state/status/cpu/memory section + fn check_expected(expected: [&str; 6], w: u16, _h: u16, setup: &TuiTestSetup, color: Color) { + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if (145..=207).contains(&index) { + assert_eq!(result_cell.fg, color); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + } + } + } + + #[test] + // No containers, panel unselected, then selected, border color changes correctly + fn test_draw_blocks_containers_none() { + let (w, h) = (25, 6); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers = StatefulList::new(vec![]); + + let expected = [ + "╭ Containers ───────────╮", + "│ no containers running │", + "│ │", + "│ │", + "│ │", + "╰───────────────────────╯", + ]; + + setup.gui_state.lock().next_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + } + } + + setup.gui_state.lock().previous_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::LightCyan); + } + } + } + } + + #[test] + // Containers panel drawn, selected line is bold, border is blue + fn test_draw_blocks_containers_some() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + // result matches expected + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // Selected container is bold + match index { + 131 | 133..=258 => assert_eq!(result_cell.modifier, Modifier::BOLD), + _ => { + assert!(result_cell.modifier.is_empty()); + } + } + + // Border is blue + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::LightCyan); + } + } + } + + // Change selected panel, border is now no longer blue + setup.gui_state.lock().next_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // Border is gray + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + + #[test] + /// ALl columns on all rows are coloured correctly + fn test_draw_blocks_containers_colors() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let index_blue = [ + 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 208, 209, 210, 211, 212, 213, + 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, + ]; + let index_blue = index_blue + .iter() + .flat_map(|&x| vec![x, x + 130, x + 260]) + .collect::>(); + let index_green = [ + 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, + 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, + 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, + 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, + ]; + let index_green = index_green + .iter() + .flat_map(|&x| vec![x, x + 130, x + 260]) + .collect::>(); + + let index_rx = [229, 230, 231, 232, 233, 234, 235, 236, 237, 238]; + let index_rx = index_rx + .iter() + .flat_map(|&x| vec![x, x + 130, x + 260]) + .collect::>(); + + let index_tx = [239, 240, 241, 242, 243, 244, 245, 246, 247, 248]; + let index_tx = index_tx + .iter() + .flat_map(|&x| vec![x, x + 130, x + 260]) + .collect::>(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + + let result_cell = &result[index]; + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + match index { + _x if index_blue.contains(&index) => { + assert_eq!(result_cell.fg, Color::Blue); + } + _x if index_green.contains(&index) => { + assert_eq!(result_cell.fg, Color::Green); + } + _x if index_rx.contains(&index) => { + assert_eq!(result_cell.fg, Color::Rgb(255, 233, 193)); + } + _x if index_tx.contains(&index) => { + assert_eq!(result_cell.fg, Color::Rgb(205, 140, 140)); + } + (0..=130) | (259..=260) | (389..=390) | (519..=520) | (649..=779) => { + assert_eq!(result_cell.fg, Color::LightCyan); + } + _ => { + assert_eq!(result_cell.fg, Color::Reset); + } + } + } + } + } + + #[test] + /// When long container/image name, it is truncated correctly + fn test_draw_blocks_containers_long_name_image() { + let (w, h) = (170, 6); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers.items[0].name = + ContainerName::from("a_long_container_name_for_the_purposes_of_this_test"); + setup.app_data.lock().containers.items[0].image = + ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test"); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ a_long_container_name_for_the… ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 a_long_image_name_for_the_pur… 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup.app_data.lock().containers.items[0].state = State::Paused; + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + } + } + + // THis char: … + } + + #[test] + /// When container is paused, correct colors displayed + fn test_draw_blocks_containers_paused() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ॥ paused Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup.app_data.lock().containers.items[0].state = State::Paused; + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + check_expected(expected, w, h, &setup, Color::Yellow); + } + + #[test] + /// When container is dead, correct colors displayed + fn test_draw_blocks_containers_dead() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✖ dead Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Dead; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + check_expected(expected, w, h, &setup, Color::Red); + } + + #[test] + /// When container is exited, correct colors displayed + fn test_draw_blocks_containers_exited() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ✖ exited Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Exited; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + check_expected(expected, w, h, &setup, Color::Red); + } + #[test] + /// When container is paused, correct colors displayed + fn test_draw_blocks_containers_removing() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 removing Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Removing; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + check_expected(expected, w, h, &setup, Color::LightRed); + } + #[test] + /// When container state is restarting, correct colors displayed + fn test_draw_blocks_containers_restarting() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ↻ restarting Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Restarting; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + check_expected(expected, w, h, &setup, Color::LightGreen); + } + #[test] + /// When container state is unknown, correct colors displayed + fn test_draw_blocks_containers_unknown() { + let (w, h) = (130, 6); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + "╭ Containers 1/3 ────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│⚪ container_1 ? unknown Up 1 hour 00.00% 0.00 kB / 0.00 kB 1 image_1 0.00 kB 0.00 kB │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB │", + "│ │", + "╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + ]; + setup.app_data.lock().containers.items[0].state = State::Unknown; + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::containers(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + check_expected(expected, w, h, &setup, Color::Red); + } + // ********** // + // Logs panel // + // ********** // + + #[test] + // No logs, panel unselected, then selected, border color changes correctly + fn test_draw_blocks_logs_none() { + let (w, h) = (25, 6); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers = StatefulList::new(vec![]); + + let expected = [ + "╭ Logs ─────────────────╮", + "│ no logs found │", + "│ │", + "│ │", + "│ │", + "╰───────────────────────╯", + ]; + + let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + } + } + + setup.gui_state.lock().next_panel(); + setup.gui_state.lock().next_panel(); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + // When selected, has a blue border + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::LightCyan); + } + } + } + } + + #[test] + // Parsing logs, spinner visible, and then animates by one frame + fn test_draw_blocks_logs_parsing() { + let (w, h) = (25, 6); + let mut setup = test_setup(w, h, true, true); + let uuid = Uuid::new_v4(); + setup.gui_state.lock().next_loading(uuid); + + let expected = [ + "╭ Logs - container_1 ───╮", + "│ parsing logs ⠙ │", + "│ │", + "│ │", + "│ │", + "╰───────────────────────╯", + ]; + + let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + fd.init = true; + + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let test = |terminal: &Terminal, expected: [&str; 6]| { + let result = &terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + } + } + }; + + test(&setup.terminal, expected); + + // animation moved by one frame + setup.gui_state.lock().next_loading(uuid); + + let expected = [ + "╭ Logs - container_1 ───╮", + "│ parsing logs ⠹ │", + "│ │", + "│ │", + "│ │", + "╰───────────────────────╯", + ]; + + let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + fd.init = true; + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + test(&setup.terminal, expected); + } + + #[test] + // Logs correct displayed, changing log state also draws correctly + fn test_draw_blocks_logs_some() { + let (w, h) = (25, 6); + let mut setup = test_setup(w, h, true, true); + + insert_logs(&setup); + + let test = |terminal: &Terminal, + expected: [&str; 6], + range: RangeInclusive| { + let result = &terminal.backend().buffer().content; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.fg, Color::Reset); + + if range.contains(&index) { + assert_eq!(result_cell.modifier, Modifier::BOLD); + } else { + assert!(result_cell.modifier.is_empty()); + } + } + } + }; + + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + let expected = [ + "╭ Logs 3/3 - container_1╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "╰───────────────────────╯", + ]; + test(&setup.terminal, expected, 76..=98); + + // Change selected log line + setup.app_data.lock().log_previous(); + let _fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &setup.fd, &setup.gui_state); + }) + .unwrap(); + + let expected = [ + "╭ Logs 2/3 - container_1╮", + "│ line 1 │", + "│▶ line 2 │", + "│ line 3 │", + "│ │", + "╰───────────────────────╯", + ]; + test(&setup.terminal, expected, 51..=73); + } + + #[test] + // Full (long) name displayed in logs border + fn test_draw_blocks_logs_long_name() { + let (w, h) = (80, 6); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers.items[0].name = + ContainerName::from("a_long_container_name_for_the_purposes_of_this_test"); + setup.app_data.lock().containers.items[0].image = + ContainerImage::from("a_long_image_name_for_the_purposes_of_this_test"); + + insert_logs(&setup); + + let expected = [ + "╭ Logs 3/3 - a_long_container_name_for_the_purposes_of_this_test ──────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────╯", + ]; + + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + setup + .terminal + .draw(|f| { + super::logs(&setup.app_data, setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + } + } + } + + // ************ // + // Charts panel // + // ************ // + + const EXPECTED: [&str; 10] = [ + "╭───────────── cpu 03.00% ─────────────╮╭────────── memory 30.00 kB ───────────╮", + "│10.00%│ • ││100.00 kB│ •• │", + "│ │ •• ││ │ •• │", + "│ │ ••• ││ │ • • │", + "│ │ • • ││ │ • • │", + "│ │ • •• ││ │•• •• │", + "│ │• • ││ │• • │", + "│ │• • ││ │• • │", + "│ │ ││ │ │", + "╰──────────────────────────────────────╯╰──────────────────────────────────────╯", + ]; + const MEMORY_INDEX: [usize; 16] = [ + 134, 135, 214, 215, 293, 295, 372, 375, 451, 452, 455, 456, 531, 535, 611, 615, + ]; + + const CPU_INDEX: [usize; 15] = [ + 92, 171, 172, 250, 251, 252, 330, 332, 409, 413, 414, 488, 493, 568, 573, + ]; + + #[allow(clippy::cast_precision_loss)] + // Add fixed data to the cpu & mem vecdeques, that match the above data + fn insert_chart_data(setup: &TuiTestSetup) { + for i in 1..=10 { + setup.app_data.lock().update_stats_by_id( + &setup.ids[0], + Some(i as f64), + Some(i * 10000), + i * 10000, + i, + i, + ); + } + for i in 1..=3 { + setup.app_data.lock().update_stats_by_id( + &setup.ids[0], + Some(i as f64), + Some(i * 10000), + i * 10000, + i, + i, + ); + } + } + #[test] + // When status is Running, but not data, charts drawn without dots etc + fn test_draw_blocks_charts_running_none() { + let (w, h) = (80, 10); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::chart(f, setup.area, &setup.app_data); + }) + .unwrap(); + + let expected = [ + "╭───────────── cpu 00.00% ─────────────╮╭─────────── memory 0.00 kB ───────────╮", + "│00.00%│ ││0.00 kB│ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "│ │ ││ │ │", + "╰──────────────────────────────────────╯╰──────────────────────────────────────╯", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + match index { + // chart tiles - cpu 03.00% && memory 30.00 kB - are green + 14..=25 | 52..=67 => { + assert_eq!(result_cell.fg, Color::Green); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // Cpu & Memory max are orange and bold + 81..=86 | 121..=127 => { + assert_eq!(result_cell.fg, ORANGE); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // All others + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + #[test] + // When status is Running, charts correctly drawn + fn test_draw_blocks_charts_running_some() { + let (w, h) = (80, 10); + let mut setup = test_setup(w, h, true, true); + + insert_chart_data(&setup); + + setup + .terminal + .draw(|f| { + super::chart(f, setup.area, &setup.app_data); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in EXPECTED.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + match index { + // chart tiles - cpu 03.00% && memory 30.00 kB - are green + 14..=25 | 51..=67 => { + assert_eq!(result_cell.fg, Color::Green); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // Cpu & Memory max are orange and bold + 81..=86 | 121..=129 => { + assert_eq!(result_cell.fg, ORANGE); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // cpu dots are magenta + _x if CPU_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Magenta); + assert!(result_cell.modifier.is_empty()); + } + // memory dots are cyan + _x if MEMORY_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Cyan); + assert!(result_cell.modifier.is_empty()); + } + // All others + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + #[test] + // Whens status paused, some text is now Yellow + fn test_draw_blocks_charts_paused() { + let (w, h) = (80, 10); + let mut setup = test_setup(w, h, true, true); + + insert_chart_data(&setup); + setup.app_data.lock().containers.items[0].state = State::Paused; + + setup + .terminal + .draw(|f| { + super::chart(f, setup.area, &setup.app_data); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in EXPECTED.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + match index { + // Titles and y axis are yellow + 14..=25 | 51..=67 | 81..=86 | 121..=129 => { + assert_eq!(result_cell.fg, Color::Yellow); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + _x if CPU_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Magenta); + assert!(result_cell.modifier.is_empty()); + } + // memory dots are cyan + _x if MEMORY_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Cyan); + assert!(result_cell.modifier.is_empty()); + } + // All others + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + #[test] + // When dead, text is read + fn test_draw_blocks_charts_dead() { + let (w, h) = (80, 10); + let mut setup = test_setup(w, h, true, true); + insert_chart_data(&setup); + setup.app_data.lock().containers.items[0].state = State::Dead; + + setup + .terminal + .draw(|f| { + super::chart(f, setup.area, &setup.app_data); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in EXPECTED.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + match index { + // Titles and y axis are red + 14..=25 | 51..=67 | 81..=86 | 121..=129 => { + assert_eq!(result_cell.fg, Color::Red); + assert_eq!(result_cell.modifier, Modifier::BOLD); + } + // cpu dots are magenta + _x if CPU_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Magenta); + assert!(result_cell.modifier.is_empty()); + } + // memory dots are cyan + _x if MEMORY_INDEX.contains(&index) => { + assert_eq!(result_cell.fg, Color::Cyan); + assert!(result_cell.modifier.is_empty()); + } + // All others + _ => { + assert_eq!(result_cell.fg, Color::Reset); + assert!(result_cell.modifier.is_empty()); + } + } + } + } + } + + // ******* // + // Headers // + // ******* // + + #[test] + /// Heading back only has show/exit help when no containers, correctly coloured + fn test_draw_blocks_headers_no_containers() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + setup.app_data.lock().containers = StatefulList::new(vec![]); + + let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + let expected = " ( h ) show help "; + + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::White); + } + + fd.help_visible = true; + let expected = " ( h ) exit help "; + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!(result_cell.fg, Color::Black); + } + } + + #[test] + /// Show all headings when containers present, colors valid + fn test_draw_blocks_headers_some_containers() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + let expected = " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "; + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match index { + (2..=122) => Color::Black, + _ => Color::White, + } + ); + } + } + + #[test] + /// Test all combination of headers & sort by + fn test_draw_blocks_headers_sort_containers() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + let mut fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + let mut test = |expected: &str, range: RangeInclusive, x: (Header, SortedOrder)| { + fd.sorted_by = Some(x); + + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match index { + 0 | 1 => Color::White, + // given range | help section + x if range.contains(&x) || (123..=139).contains(&x) => Color::White, + _ => Color::Black, + } + ); + } + }; + + // Name + test(" ▲ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 1..=14, (Header::Name, SortedOrder::Asc)); + test(" ▼ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 1..=14, (Header::Name, SortedOrder::Desc)); + + // state + test(" name ▲ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 15..=26, (Header::State, SortedOrder::Asc)); + test(" name ▼ state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 15..=26, (Header::State, SortedOrder::Desc)); + + // status + test(" name state ▲ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 27..=47, (Header::Status, SortedOrder::Asc)); + test(" name state ▼ status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 27..=47, (Header::Status, SortedOrder::Desc)); + + // cpu + test(" name state status ▲ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 48..=57, (Header::Cpu, SortedOrder::Asc)); + test(" name state status ▼ cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", 48..=57, (Header::Cpu, SortedOrder::Desc)); + + // mem + test(" name state status cpu ▲ memory/limit id image ↓ rx ↑ tx ( h ) show help ", 58..=77, (Header::Memory, SortedOrder::Asc)); + test(" name state status cpu ▼ memory/limit id image ↓ rx ↑ tx ( h ) show help ", 58..=77, (Header::Memory, SortedOrder::Desc)); + + // id + test(" name state status cpu memory/limit ▲ id image ↓ rx ↑ tx ( h ) show help ", 78..=88, (Header::Id, SortedOrder::Asc)); + test(" name state status cpu memory/limit ▼ id image ↓ rx ↑ tx ( h ) show help ", 78..=88, (Header::Id, SortedOrder::Desc)); + + // image + test(" name state status cpu memory/limit id ▲ image ↓ rx ↑ tx ( h ) show help ", 89..=98, (Header::Image, SortedOrder::Asc)); + test(" name state status cpu memory/limit id ▼ image ↓ rx ↑ tx ( h ) show help ", 89..=98, (Header::Image, SortedOrder::Desc)); + + // rx + test(" name state status cpu memory/limit id image ▲ ↓ rx ↑ tx ( h ) show help ", 99..=108, (Header::Rx, SortedOrder::Asc)); + test(" name state status cpu memory/limit id image ▼ ↓ rx ↑ tx ( h ) show help ", 99..=108, (Header::Rx, SortedOrder::Desc)); + + // tx + test(" name state status cpu memory/limit id image ↓ rx ▲ ↑ tx ( h ) show help ", 109..=122, (Header::Tx, SortedOrder::Asc)); + test(" name state status cpu memory/limit id image ↓ rx ▼ ↑ tx ( h ) show help ", 109..=122, (Header::Tx, SortedOrder::Desc)); + } + + #[test] + /// Show animation + fn test_draw_blocks_headers_animation() { + let (w, h) = (140, 1); + let mut setup = test_setup(w, h, true, true); + let uuid = Uuid::new_v4(); + setup.gui_state.lock().next_loading(uuid); + let fd = FrameData::from((setup.app_data.lock(), setup.gui_state.lock())); + + setup + .terminal + .draw(|f| { + super::heading_bar(setup.area, f, &fd, &setup.gui_state); + }) + .unwrap(); + + let expected = " ⠙ name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help "; + + let result = &setup.terminal.backend().buffer().content; + for (index, expected_char) in expected.chars().enumerate() { + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + assert_eq!(result_cell.bg, Color::Magenta); + assert_eq!( + result_cell.fg, + match index { + (2..=122) => Color::Black, + _ => Color::White, + } + ); + } + } + + // ********** // + // Help popup // + // ********** // + #[test] + // This will cause issues onces the version has more than the current 5 chars (0.5.0) + // Help popup is drawn correctly + fn test_draw_blocks_help() { + let (w, h) = (87, 30); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::help_box(f); + }) + .unwrap(); + + let expected = [ + " ".to_owned(), + format!(" ╭ {VERSION} ────────────────────────────────────────────────────────────────────────────╮ "), + " │ │ ".to_owned(), + " │ 88 │ ".to_owned(), + " │ 88 │ ".to_owned(), + " │ 88 │ ".to_owned(), + " │ ,adPPYba, 8b, ,d8 88 ,d8 ,adPPYba, 8b,dPPYba, │ ".to_owned(), + r#" │ a8" "8a `Y8, ,8P' 88 ,a8" a8P_____88 88P' "Y8 │ "#.to_owned(), + r#" │ 8b d8 )888( 8888[ 8PP""""""" 88 │ "#.to_owned(), + r#" │ "8a, ,a8" ,d8" "8b, 88`"Yba, "8b, ,aa 88 │ "#.to_owned(), + r#" │ `"YbbdP"' 8P' `Y8 88 `Y8a `"Ybbd8"' 88 │ "#.to_owned(), + " │ │ ".to_owned(), + " │ A simple tui to view & control docker containers │ ".to_owned(), + " │ │ ".to_owned(), + " │ ( tab ) or ( shift+tab ) change panels │ ".to_owned(), + " │ ( ↑ ↓ ) or ( j k ) or ( PgUp PgDown ) or ( Home End ) change selected line │ ".to_owned(), + " │ ( enter ) send docker container command │ ".to_owned(), + " │ ( e ) exec into a container │ ".to_owned(), + " │ ( h ) toggle this help information │ ".to_owned(), + " │ ( s ) save logs to file │ ".to_owned(), + " │ ( m ) toggle mouse capture - if disabled, text on screen can be selected & copied │ ".to_owned(), + " │ ( 0 ) stop sort │ ".to_owned(), + " │ ( 1 - 9 ) sort by header - or click header │ ".to_owned(), + " │ ( q ) quit at any time │ ".to_owned(), + " │ │ ".to_owned(), + " │ currently an early work in progress, all and any input appreciated │ ".to_owned(), + " │ https://github.com/mrjackwills/oxker │ ".to_owned(), + " │ │ ".to_owned(), + " ╰───────────────────────────────────────────────────────────────────────────────────╯ ".to_owned(), + " ".to_owned(), + ]; + + for (row_index, row) in expected.iter().enumerate() { + let mut bracket_key = vec![]; + let mut push_bracket_key = false; + + let result = &setup.terminal.backend().buffer().content; + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + let result_str = result_cell.symbol(); + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + // First and last row, and first char and last char in each row, is empty + if row_index == 0 + || row_index == usize::from(h - 1) + || char_index == 0 + || char_index == usize::from(w - 1) + { + assert_eq!(result_cell.fg, Color::Reset); + assert_eq!(result_cell.bg, Color::Reset); + // Borders + } else if BORDER_CHARS.contains(&result_str) { + assert_eq!(result_cell.fg, Color::Black); + assert_eq!(result_cell.bg, Color::Magenta); + // everything else has a magenta background + } else { + assert_eq!(result_cell.bg, Color::Magenta); + } + + // check that ( [key] ) is white + if result_str == "(" { + push_bracket_key = true; + bracket_key.push(result_cell); + } + if push_bracket_key { + bracket_key.push(result_cell); + if result_str == ")" { + push_bracket_key = false; + for i in &bracket_key { + assert_eq!(i.fg, Color::White); + } + bracket_key.clear(); + } + } + // TODO should really be testing every color of every str here + } + } + } + + // ************ // + // Delete popup // + // ************ // + + #[test] + // Delete container popup is drawn correctly + fn test_draw_blocks_delete() { + let (w, h) = (82, 10); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + " ", + " ╭──────────────────────── Confirm Delete ────────────────────────╮ ", + " │ │ ", + " │ Are you sure you want to delete container: container_1 │ ", + " │ │ ", + " │ ╭─────────────────────╮ ╭─────────────────────╮ │ ", + " │ │ (N)o │ │ (Y)es │ │ ", + " │ ╰─────────────────────╯ ╰─────────────────────╯ │ ", + " ╰────────────────────────────────────────────────────────────────╯ ", + " ", + ]; + + setup + .terminal + .draw(|f| { + super::delete_confirm(f, &setup.gui_state, &ContainerName::from("container_1")); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + if row_index == 0 + || row_index == usize::from(h - 1) + || char_index < 8 + || char_index > usize::from(w - 9) + { + assert_eq!(result_cell.fg, Color::Reset); + assert_eq!(result_cell.bg, Color::Reset); + } else { + assert_eq!(result_cell.bg, Color::White); + } + + // Borders are black + if BORDER_CHARS.contains(&result_cell.symbol()) { + assert_eq!(result_cell.fg, Color::Black); + // Container name is red + } else if row_index == 3 && (57..=67).contains(&char_index) { + assert_eq!(result_cell.fg, Color::Red); + // All other text is black + } else if !row_index == 0 + && !row_index == usize::from(h - 1) + && !char_index < 8 + && !char_index > usize::from(w - 9) + { + assert_eq!(result_cell.fg, Color::Black); + } + } + } + } + + // ***** // + // popup // + // ***** // + + #[test] + /// Info box drawn in bottom right + fn test_draw_blocks_info() { + let (w, h) = (45, 9); + let mut setup = test_setup(w, h, true, true); + + let expected = [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " test ", + " ", + ]; + + setup + .terminal + .draw(|f| { + super::info(f, "test", std::time::Instant::now(), &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(expected_char.to_string(), result_cell.symbol()); + + let (fg, bg) = if row_index >= 6 && char_index >= 32 { + (Color::White, Color::Blue) + } else { + (Color::Reset, Color::Reset) + }; + + assert_eq!(result_cell.fg, fg); + assert_eq!(result_cell.bg, bg); + } + } + } + + // *********** // + // Error popup // + // *********** // + + #[test] + // Test that the error popup is centered, red background, white border, white text, and displays the correct text + fn test_draw_blocks_docker_connect_error() { + let (w, h) = (46, 9); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::error(f, AppError::DockerConnect, Some(4)); + }) + .unwrap(); + + let expected = vec![ + " ".to_owned(), + " ╭───────────────── Error ──────────────────╮ ".to_owned(), + " │ │ ".to_owned(), + " │ Unable to access docker daemon │ ".to_owned(), + " │ │ ".to_owned(), + format!(" │ oxker::v{VERSION} closing in 04 seconds │ "), + " │ │ ".to_owned(), + " ╰──────────────────────────────────────────╯ ".to_owned(), + " ".to_owned(), + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + + if (1..=usize::from(h) - 2).contains(&row_index) + && (1..=usize::from(w) - 2).contains(&char_index) + { + assert_eq!(result_cell.bg, Color::Red); + } + if result_cell + .symbol() + .chars() + .next() + .unwrap() + .is_alphanumeric() + { + assert_eq!(result_cell.fg, Color::White); + } + } + } + } + + #[test] + // Test that the clearable error popup is centered, red background, white border, white text, and displays the correct text + fn test_draw_blocks_clearable_error() { + let (w, h) = (39, 10); + let mut setup = test_setup(w, h, true, true); + + setup + .terminal + .draw(|f| { + super::error(f, AppError::DockerExec, Some(4)); + }) + .unwrap(); + + let expected = [ + " ", + " ╭────────────── Error ──────────────╮ ", + " │ │ ", + " │ Unable to exec into container │ ", + " │ │ ", + " │ ( c ) clear error │ ", + " │ ( q ) quit oxker │ ", + " │ │ ", + " ╰───────────────────────────────────╯ ", + " ", + ]; + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(result_cell.symbol(), expected_char.to_string()); + if (1..=usize::from(h) - 2).contains(&row_index) + && (1..=usize::from(w) - 2).contains(&char_index) + { + assert_eq!(result_cell.bg, Color::Red); + } + if result_cell + .symbol() + .chars() + .next() + .unwrap() + .is_alphanumeric() + || ["(", ")"].contains(&result_cell.symbol()) + { + assert_eq!(result_cell.fg, Color::White); + } + } + } + } + + // *************** // + // The whole layout // + // **************** // + #[test] + // Check that the whole layout is drawn correctly + fn test_draw_blocks_the_whole_layout() { + let (w, h) = (160, 30); + let mut setup = test_setup(w, h, true, true); + + insert_chart_data(&setup); + insert_logs(&setup); + + let expected = [ + " name state status cpu memory/limit id image ↓ rx ↑ tx ( h ) show help ", + "╭ Containers 1/3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮╭──────────────╮", + "│⚪ container_1 ✓ running Up 1 hour 03.00% 30.00 kB / 30.00 kB 1 image_1 0.00 kB 0.00 kB ││▶ pause │", + "│ container_2 ✓ running Up 2 hour 00.00% 0.00 kB / 0.00 kB 2 image_2 0.00 kB 0.00 kB ││ restart │", + "│ container_3 ✓ running Up 3 hour 00.00% 0.00 kB / 0.00 kB 3 image_3 0.00 kB 0.00 kB ││ stop │", + "│ ││ delete │", + "│ ││ │", + "│ ││ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯╰──────────────╯", + "╭ Logs 3/3 - container_1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮", + "│ line 1 │", + "│ line 2 │", + "│▶ line 3 │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "│ │", + "╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯", + "╭───────────────────────────────── cpu 03.00% ─────────────────────────────────╮╭────────────────────────────── memory 30.00 kB ───────────────────────────────╮", + "│10.00%│ •••••• ││100.00 kB│ •••••• │", + "│ │••••• •••• ││ │••••• ••• │", + "│ │ ││ │ │", + "╰──────────────────────────────────────────────────────────────────────────────╯╰──────────────────────────────────────────────────────────────────────────────╯", + ]; + setup + .terminal + .draw(|f| { + draw_frame(f, &setup.app_data, &setup.gui_state); + }) + .unwrap(); + + let result = &setup.terminal.backend().buffer().content; + for (row_index, row) in expected.iter().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &result[index]; + + assert_eq!(expected_char.to_string(), result_cell.symbol()); + } + } + } } - -// Draw nothing, as in a blank screen -// pub fn nothing(f: &mut Frame) { -// let whole_layout = Layout::default() -// .direction(Direction::Vertical) -// .constraints([Constraint::Min(100)].as_ref()) -// .split(f.size()); - -// let block = Block::default() -// .borders(Borders::NONE); -// f.render_widget(block, whole_layout[0]); -// } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 1b22599..eb2586d 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -217,22 +217,6 @@ impl Ui { } } -#[cfg(not(debug_assertions))] -fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> { - Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref()) - .split(f.size()) -} - -#[cfg(debug_assertions)] -fn get_wholelayout(f: &Frame) -> std::rc::Rc<[ratatui::layout::Rect]> { - Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Min(1), Constraint::Min(1), Constraint::Min(100)].as_ref()) - .split(f.size()) -} - /// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here #[derive(Debug)] pub struct FrameData { @@ -279,21 +263,16 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData { fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc>) { let fd = FrameData::from((app_data.lock(), gui_state.lock())); - let whole_layout = get_wholelayout(f); - #[cfg(debug_assertions)] - draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string()); - - #[cfg(debug_assertions)] - let whole_layout_split = (1, 2); - - #[cfg(not(debug_assertions))] - let whole_layout_split = (0, 1); + let whole_layout = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Min(1), Constraint::Min(100)].as_ref()) + .split(f.size()); // Split into 3, containers+controls, logs, then graphs let upper_main = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref()) - .split(whole_layout[whole_layout_split.1]); + .split(whole_layout[1]); let top_split = if fd.has_containers { vec![Constraint::Percentage(90), Constraint::Percentage(10)] @@ -318,11 +297,11 @@ fn draw_frame(f: &mut Frame, app_data: &Arc>, gui_state: &Arc