From b9df4e446a4ede7f33932f0d6970a70f3e2a231e Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 13 Jan 2024 17:17:26 +0000 Subject: [PATCH 1/6] tests: AppData tests --- src/app_data/container_state.rs | 17 +- src/app_data/mod.rs | 1411 ++++++++++++++++++++++++++++--- src/docker_data/mod.rs | 2 +- src/input_handler/mod.rs | 10 +- 4 files changed, 1300 insertions(+), 140 deletions(-) diff --git a/src/app_data/container_state.rs b/src/app_data/container_state.rs index 0f81b9e..9844b30 100644 --- a/src/app_data/container_state.rs +++ b/src/app_data/container_state.rs @@ -60,6 +60,13 @@ macro_rules! unit_struct { } } + #[cfg(test)] + impl From<&str> 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, @@ -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), diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index a9fcadc..d508310 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,6 +54,27 @@ impl fmt::Display for Header { } } +#[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, +} + impl AppData { #[cfg(debug_assertions)] pub fn get_debug_string(&self) -> &str { @@ -87,27 +87,6 @@ impl AppData { self.debug_string.push_str(x); } - /// 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; - self.sort_containers(); - self.containers - .state - .select(self.containers.items.iter().position(|i| { - self.get_selected_container_id() - .map_or(false, |id| i.id == id) - })); - } - - /// 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 { @@ -131,8 +110,29 @@ 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() + } + /// 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; + self.sort_containers(); + self.containers + .state + .select(self.containers.items.iter().position(|i| { + self.get_selected_container_id() + .map_or(false, |id| i.id == id) + })); + } + /// Remove the sorted header & order, and sort by default - created datetime pub fn reset_sorted(&mut self) { self.set_sorted(None); @@ -237,6 +237,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 +267,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 +288,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 +328,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 +369,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 +376,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::new() + } else { + format!("{logs_len} ") + }; + format!("{}- {}", prefix, ci.name.get()) + }) } /// select next selected log line @@ -389,19 +416,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 +434,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 @@ -485,36 +510,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 +658,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 +682,1159 @@ impl AppData { } } } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)] +mod tests { + + use std::collections::VecDeque; + + use super::*; + + 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, + } + } + + 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"), + ) + } + + fn gen_appdata(containers: &[ContainerItem]) -> AppData { + AppData { + containers: StatefulList::new(containers.to_vec()), + error: None, + sorted_by: None, + debug_string: String::new(), + args: gen_args(), + } + } + + 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) + } + + // ******** // + // 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 mut 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, mut 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, mut 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, mut 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, mut 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, mut 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, mut 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, mut 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 mut 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![ + ContainerSummary { + id: Some("1".to_owned()), + names: Some(vec!["container_1".to_owned()]), + image: Some("image_1".to_owned()), + image_id: Some("1".to_owned()), + command: None, + created: Some(1), + ports: None, + size_rw: None, + size_root_fs: None, + labels: None, + state: Some("paused".to_owned()), + status: Some("Up 1 hour".to_owned()), + host_config: None, + network_settings: None, + mounts: None, + }, + ContainerSummary { + id: Some("2".to_owned()), + names: Some(vec!["container_2".to_owned()]), + image: Some("image_2".to_owned()), + image_id: Some("2".to_owned()), + command: None, + created: Some(2), + ports: None, + size_rw: None, + size_root_fs: None, + labels: None, + state: Some("dead".to_owned()), + status: Some("Up 2 hour".to_owned()), + host_config: None, + network_settings: None, + mounts: None, + }, + ]; + + 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..1279cbe 100644 --- a/src/docker_data/mod.rs +++ b/src/docker_data/mod.rs @@ -150,7 +150,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); diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index 1f7fad0..d6736f3 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -268,7 +268,7 @@ 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 @@ -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(), } } } From 52d1610ac6bad7c17b1c4384230df639f8dace8d Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 13 Jan 2024 22:50:37 +0000 Subject: [PATCH 2/6] fix: testing dockerfile change name --- create_release.sh | 6 +++--- docker-compose.yml | 14 +++++++------- postgres.Dockerfile | 1 + 3 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 postgres.Dockerfile diff --git a/create_release.sh b/create_release.sh index f3c90c2..13e8a24 100755 --- a/create_release.sh +++ b/create_release.sh @@ -215,19 +215,19 @@ check_typos() { } # Make sure the unused lint isn't used -check_allow_unsued() { +check_allow_unused() { matches_any=$(find . -type d \( -name .git -o -name target \) -prune -o -type f -exec grep -lE '^#!\[allow\(unused\)\]$' {} +) matches_cargo=$(grep "^unused = \"allow\"" ./Cargo.toml) if [ -n "$matches_any" ]; then error_close "\"#[allow(unused)]\" in ${matches_any}" elif [ -n "$matches_cargo" ]; then - error_close "\"unsed = \"allow\"\" in Cargo.toml" + error_close "\"unused = \"allow\"\" in Cargo.toml" fi } # Full flow to create a new release release_flow() { - check_allow_unsued + check_allow_unused check_typos check_git diff --git a/docker-compose.yml b/docker-compose.yml index b2547f0..7553ceb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,16 +3,18 @@ networks: oxker-example-net: name: oxker-examaple-net services: - postgres: - image: postgres:alpine3.19 - container_name: postgres + postgres_but_with_a_longer_container_name: + container_name: postgres_but_with_a_longer_container_name + build: + dockerfile: ./postgres.Dockerfile + context: "." environment: - - POSTGRES_PASSWORD=never_use_this_password_in_production + - POSTGRES_PASSWORD=never_use_this_password_in_production ipc: private restart: always shm_size: 256MB networks: - - oxker-example-net + - oxker-example-net deploy: resources: limits: @@ -39,5 +41,3 @@ services: resources: limits: memory: 512M - - diff --git a/postgres.Dockerfile b/postgres.Dockerfile new file mode 100644 index 0000000..7c27a09 --- /dev/null +++ b/postgres.Dockerfile @@ -0,0 +1 @@ +FROM postgres:16-alpine3.19 \ No newline at end of file From a34c046dee8d5f4b3eeb9c14d42b8f31047e01a5 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sat, 13 Jan 2024 23:01:22 +0000 Subject: [PATCH 3/6] fix: is_oxker_in_container() Check is both is_oxker and running in a container, so that oxker container commands will only be ignored if both are true --- src/app_data/mod.rs | 15 ++++++++++----- src/input_handler/mod.rs | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/app_data/mod.rs b/src/app_data/mod.rs index d508310..485eee2 100644 --- a/src/app_data/mod.rs +++ b/src/app_data/mod.rs @@ -470,6 +470,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 { @@ -1815,12 +1821,11 @@ mod tests { }, ]; - app_data.update_containers(&mut input); - let result_post = app_data.get_container_items(); - assert_ne!(&result_pre, result_post); + 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); - + assert_eq!(result_post[1].state, State::Dead); } #[test] diff --git a/src/input_handler/mod.rs b/src/input_handler/mod.rs index d6736f3..ef630b9 100644 --- a/src/input_handler/mod.rs +++ b/src/input_handler/mod.rs @@ -272,7 +272,7 @@ impl InputHandler { 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(); From a68794f78d38fa653257d3a462bf61bd22ce2a94 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 14 Jan 2024 00:22:26 +0000 Subject: [PATCH 4/6] wip: terminal tests --- src/ui/draw_blocks.rs | 80 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index 708f197..abb34ee 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -851,8 +851,7 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co .update_region_map(Region::Delete(DeleteButton::Yes), yes_area); } -/// Draw an error popup over whole screen -pub fn error(f: &mut Frame, error: AppError, seconds: Option) { +fn gen_error<'f>(f: &mut Frame, error: AppError, seconds: Option) -> (Paragraph<'f>, Rect) { let block = Block::default() .title(" Error ") .border_type(BorderType::Rounded) @@ -889,6 +888,13 @@ pub fn error(f: &mut Frame, error: AppError, seconds: Option) { .alignment(Alignment::Center); let area = popup(lines, max_line_width, f.size(), BoxLocation::MiddleCentre); + (paragraph, area) + // area +} + +/// Draw an error popup over whole screen +pub fn error(f: &mut Frame, error: AppError, seconds: Option) { + let (paragraph, area) = gen_error(f, error, seconds); f.render_widget(Clear, area); f.render_widget(paragraph, area); } @@ -969,3 +975,73 @@ pub fn debug_bar(area: Rect, f: &mut Frame, debug_string: &str) { // .borders(Borders::NONE); // f.render_widget(block, whole_layout[0]); // } + +#[cfg(test)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)] +mod tests { + + use std::collections::VecDeque; + + use ratatui::{backend::TestBackend, buffer::Buffer, Terminal}; + + use super::*; + + #[test] + // Test that the error popup is centered, red background, white border, white text, and displays the correct text + fn test_draw_blocks_error() { + let backend = TestBackend::new(46, 9); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let block = super::gen_error(f, AppError::DockerConnect, Some(4)); + f.render_widget(block.0, block.1); + }) + .unwrap(); + + let mut 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(), + ]; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, char) in row.chars().enumerate() { + let index = row_index * 46 + char_index; + let result_char = &terminal.backend().buffer().content[index]; + assert_eq!(char.to_string(), result_char.symbol()); + if (1..=7).contains(&row_index) && (1..=44).contains(&char_index) { + assert_eq!(result_char.bg, Color::Red); + } + if result_char.symbol().chars().next().unwrap().is_alphanumeric() { + assert_eq!(result_char.fg, Color::White); + } + } + } + } + // let result = &terminal.backend().buffer().content.iter().map(|i|i.symbol().to_owned()).collect::>(); + // println!("{expected:?}"); + // println!("{:?}", terminal.backend().buffer().content); + // // let mut expected = Buffer::with_lines(vec![ + // // "โ”ŒTitleโ”€โ” ", + // // "โ”‚ โ”‚ ", + // // "โ”‚ โ”‚ ", + // // "โ”‚ โ”‚ ", + // // "โ”‚ โ”‚ ", + // // "โ”‚ โ”‚ ", + // // "โ”‚ โ”‚ ", + // // "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ", + // // " ", + // // " ", + // // ]); + // // for x in 1..=5 { + // // expected.get_mut(x, 0).set_fg(Color::LightBlue); + // // } + // terminal.backend().assert_buffer(&expected); +} From 8e9243d884dc73e515ac5fad9a0a1d906b689bd1 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 14 Jan 2024 00:31:38 +0000 Subject: [PATCH 5/6] test: terminal wip --- src/ui/draw_blocks.rs | 69 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 59 insertions(+), 10 deletions(-) diff --git a/src/ui/draw_blocks.rs b/src/ui/draw_blocks.rs index abb34ee..600d353 100644 --- a/src/ui/draw_blocks.rs +++ b/src/ui/draw_blocks.rs @@ -988,8 +988,9 @@ mod tests { #[test] // Test that the error popup is centered, red background, white border, white text, and displays the correct text - fn test_draw_blocks_error() { - let backend = TestBackend::new(46, 9); + fn test_draw_blocks_docker_connect_error() { + let (w,h) = (46,9); + let backend = TestBackend::new(w, h); let mut terminal = Terminal::new(backend).unwrap(); terminal @@ -1013,15 +1014,63 @@ mod tests { for (row_index, row) in expected.iter().enumerate() { for (char_index, char) in row.chars().enumerate() { - let index = row_index * 46 + char_index; - let result_char = &terminal.backend().buffer().content[index]; - assert_eq!(char.to_string(), result_char.symbol()); - if (1..=7).contains(&row_index) && (1..=44).contains(&char_index) { - assert_eq!(result_char.bg, Color::Red); + let index = row_index * usize::from(w) + char_index; + let result_cell = &terminal.backend().buffer().content[index]; + assert_eq!(char.to_string(), result_cell.symbol()); + 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_char.symbol().chars().next().unwrap().is_alphanumeric() { - assert_eq!(result_char.fg, Color::White); - } + 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 backend = TestBackend::new(w, h); + let mut terminal = Terminal::new(backend).unwrap(); + + terminal + .draw(|f| { + let block = super::gen_error(f, AppError::DockerExec, Some(4)); + f.render_widget(block.0, block.1); + }) + .unwrap(); + + let mut expected = [ + " ", + " โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ ", + " โ”‚ โ”‚ ", + " โ”‚ Unable to exec into container โ”‚ ", + " โ”‚ โ”‚ ", + " โ”‚ ( c ) clear error โ”‚ ", + " โ”‚ ( q ) quit oxker โ”‚ ", + " โ”‚ โ”‚ ", + " โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ", + " ", + ]; + + for (row_index, row) in expected.iter().enumerate() { + for (char_index, char) in row.chars().enumerate() { + let index = row_index * usize::from(w) + char_index; + let result_cell = &terminal.backend().buffer().content[index]; + assert_eq!(char.to_string(), result_cell.symbol()); + 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); + } } } } From 53543a1b728187416c7ea52c4ec65bff074b7413 Mon Sep 17 00:00:00 2001 From: Jack Wills <32690432+mrjackwills@users.noreply.github.com> Date: Sun, 14 Jan 2024 10:06:41 +0000 Subject: [PATCH 6/6] tests: gui tests --- README.md | 3 +- src/app_data/container_state.rs | 97 +- src/app_data/mod.rs | 242 ++-- src/docker_data/mod.rs | 148 ++- src/main.rs | 79 ++ src/ui/color_match.rs | 76 ++ src/ui/draw_blocks.rs | 1871 +++++++++++++++++++++++++++++-- src/ui/mod.rs | 35 +- 8 files changed, 2235 insertions(+), 316 deletions(-) diff --git a/README.md b/README.md index e1d47d5..a1a6f2c 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,8 @@ see ( .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(), ) @@ -851,7 +850,8 @@ pub fn delete_confirm(f: &mut Frame, gui_state: &Arc>, name: &Co .update_region_map(Region::Delete(DeleteButton::Yes), yes_area); } -fn gen_error<'f>(f: &mut Frame, error: AppError, seconds: Option) -> (Paragraph<'f>, Rect) { +/// Draw an error popup over whole screen +pub fn error(f: &mut Frame, error: AppError, seconds: Option) { let block = Block::default() .title(" Error ") .border_type(BorderType::Rounded) @@ -888,13 +888,8 @@ fn gen_error<'f>(f: &mut Frame, error: AppError, seconds: Option) -> (Paragr .alignment(Alignment::Center); let area = popup(lines, max_line_width, f.size(), BoxLocation::MiddleCentre); - (paragraph, area) - // area -} -/// Draw an error popup over whole screen -pub fn error(f: &mut Frame, error: AppError, seconds: Option) { - let (paragraph, area) = gen_error(f, error, seconds); + // let (paragraph, area) = gen_error(f, error, seconds); f.render_widget(Clear, area); f.render_widget(paragraph, area); } @@ -954,53 +949,1655 @@ 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); -} - -// 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]); -// } - #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)] +#[allow(clippy::unwrap_used, clippy::many_single_char_names)] mod tests { - use std::collections::VecDeque; + use std::{ops::RangeInclusive, sync::Arc}; - use ratatui::{backend::TestBackend, buffer::Buffer, Terminal}; + use parking_lot::Mutex; + use ratatui::{ + backend::TestBackend, + layout::Rect, + style::{Color, Modifier}, + Terminal, + }; + use uuid::Uuid; - use super::*; + 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 backend = TestBackend::new(w, h); - let mut terminal = Terminal::new(backend).unwrap(); + let (w, h) = (46, 9); + let mut setup = test_setup(w, h, true, true); - terminal + setup + .terminal .draw(|f| { - let block = super::gen_error(f, AppError::DockerConnect, Some(4)); - f.render_widget(block.0, block.1); + super::error(f, AppError::DockerConnect, Some(4)); }) .unwrap(); - let mut expected = vec![ + let expected = vec![ " ".to_owned(), " โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ ".to_owned(), " โ”‚ โ”‚ ".to_owned(), @@ -1012,12 +2609,17 @@ mod tests { " ".to_owned(), ]; + let result = &setup.terminal.backend().buffer().content; for (row_index, row) in expected.iter().enumerate() { - for (char_index, char) in row.chars().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { let index = row_index * usize::from(w) + char_index; - let result_cell = &terminal.backend().buffer().content[index]; - assert_eq!(char.to_string(), result_cell.symbol()); - if (1..=usize::from(h)-2).contains(&row_index) && (1..=usize::from(w)-2).contains(&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 @@ -1036,61 +2638,114 @@ mod tests { #[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 backend = TestBackend::new(w, h); - let mut terminal = Terminal::new(backend).unwrap(); + let (w, h) = (39, 10); + let mut setup = test_setup(w, h, true, true); - terminal + setup + .terminal .draw(|f| { - let block = super::gen_error(f, AppError::DockerExec, Some(4)); - f.render_widget(block.0, block.1); + super::error(f, AppError::DockerExec, Some(4)); }) .unwrap(); - let mut expected = [ - " ", - " โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ Error โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ ", - " โ”‚ โ”‚ ", - " โ”‚ Unable to exec into container โ”‚ ", - " โ”‚ โ”‚ ", - " โ”‚ ( c ) clear error โ”‚ ", - " โ”‚ ( q ) quit oxker โ”‚ ", - " โ”‚ โ”‚ ", - " โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ ", - " ", + 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, char) in row.chars().enumerate() { + for (char_index, expected_char) in row.chars().enumerate() { let index = row_index * usize::from(w) + char_index; - let result_cell = &terminal.backend().buffer().content[index]; - assert_eq!(char.to_string(), result_cell.symbol()); - if (1..=usize::from(h)-2).contains(&row_index) && (1..=usize::from(w)-2).contains(&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); - } + 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()); } } } - // let result = &terminal.backend().buffer().content.iter().map(|i|i.symbol().to_owned()).collect::>(); - // println!("{expected:?}"); - // println!("{:?}", terminal.backend().buffer().content); - // // let mut expected = Buffer::with_lines(vec![ - // // "โ”ŒTitleโ”€โ” ", - // // "โ”‚ โ”‚ ", - // // "โ”‚ โ”‚ ", - // // "โ”‚ โ”‚ ", - // // "โ”‚ โ”‚ ", - // // "โ”‚ โ”‚ ", - // // "โ”‚ โ”‚ ", - // // "โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ", - // // " ", - // // " ", - // // ]); - // // for x in 1..=5 { - // // expected.get_mut(x, 0).set_fg(Color::LightBlue); - // // } - // terminal.backend().assert_buffer(&expected); } 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