Merge branch 'tests/init' into dev
This commit is contained in:
@@ -157,7 +157,8 @@ see <a href="https://forums.raspberrypi.com/viewtopic.php?t=203128" target='_bla
|
|||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
|
|
||||||
As of yet untested, needs work
|
~~As of yet untested, needs work~~
|
||||||
|
The work has been done
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
cargo test -- --test-threads=1
|
cargo test -- --test-threads=1
|
||||||
|
|||||||
+3
-3
@@ -215,19 +215,19 @@ check_typos() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Make sure the unused lint isn't used
|
# 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_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)
|
matches_cargo=$(grep "^unused = \"allow\"" ./Cargo.toml)
|
||||||
if [ -n "$matches_any" ]; then
|
if [ -n "$matches_any" ]; then
|
||||||
error_close "\"#[allow(unused)]\" in ${matches_any}"
|
error_close "\"#[allow(unused)]\" in ${matches_any}"
|
||||||
elif [ -n "$matches_cargo" ]; then
|
elif [ -n "$matches_cargo" ]; then
|
||||||
error_close "\"unsed = \"allow\"\" in Cargo.toml"
|
error_close "\"unused = \"allow\"\" in Cargo.toml"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Full flow to create a new release
|
# Full flow to create a new release
|
||||||
release_flow() {
|
release_flow() {
|
||||||
check_allow_unsued
|
check_allow_unused
|
||||||
check_typos
|
check_typos
|
||||||
|
|
||||||
check_git
|
check_git
|
||||||
|
|||||||
+5
-5
@@ -3,9 +3,11 @@ networks:
|
|||||||
oxker-example-net:
|
oxker-example-net:
|
||||||
name: oxker-examaple-net
|
name: oxker-examaple-net
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres_but_with_a_longer_container_name:
|
||||||
image: postgres:alpine3.19
|
container_name: postgres_but_with_a_longer_container_name
|
||||||
container_name: postgres
|
build:
|
||||||
|
dockerfile: ./postgres.Dockerfile
|
||||||
|
context: "."
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=never_use_this_password_in_production
|
- POSTGRES_PASSWORD=never_use_this_password_in_production
|
||||||
ipc: private
|
ipc: private
|
||||||
@@ -39,5 +41,3 @@ services:
|
|||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
memory: 512M
|
memory: 512M
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
FROM postgres:16-alpine3.19
|
||||||
@@ -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 {
|
impl$name {
|
||||||
pub fn get(&self) -> &str {
|
pub fn get(&self) -> &str {
|
||||||
self.0.as_str()
|
self.0.as_str()
|
||||||
@@ -93,7 +100,7 @@ macro_rules! unit_struct {
|
|||||||
unit_struct!(ContainerName);
|
unit_struct!(ContainerName);
|
||||||
unit_struct!(ContainerImage);
|
unit_struct!(ContainerImage);
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct StatefulList<T> {
|
pub struct StatefulList<T> {
|
||||||
pub state: ListState,
|
pub state: ListState,
|
||||||
pub items: Vec<T>,
|
pub items: Vec<T>,
|
||||||
@@ -234,7 +241,7 @@ impl fmt::Display for State {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Items for the container control list
|
/// Items for the container control list
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum DockerControls {
|
pub enum DockerControls {
|
||||||
Pause,
|
Pause,
|
||||||
Restart,
|
Restart,
|
||||||
@@ -416,7 +423,7 @@ impl fmt::Display for LogsTz {
|
|||||||
/// Store the logs alongside a HashSet, each log *should* generate a unique timestamp,
|
/// 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
|
/// 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
|
/// stateful list dependent on whethere the timestamp is in the HashSet or not
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Logs {
|
pub struct Logs {
|
||||||
logs: StatefulList<ListItem<'static>>,
|
logs: StatefulList<ListItem<'static>>,
|
||||||
tz: HashSet<LogsTz>,
|
tz: HashSet<LogsTz>,
|
||||||
@@ -475,7 +482,7 @@ impl Logs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Info for each container
|
/// Info for each container
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct ContainerItem {
|
pub struct ContainerItem {
|
||||||
pub created: u64,
|
pub created: u64,
|
||||||
pub cpu_stats: VecDeque<CpuStats>,
|
pub cpu_stats: VecDeque<CpuStats>,
|
||||||
@@ -594,7 +601,7 @@ impl ContainerItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Container information panel headings + widths, for nice pretty formatting
|
/// Container information panel headings + widths, for nice pretty formatting
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Columns {
|
pub struct Columns {
|
||||||
pub name: (Header, u8),
|
pub name: (Header, u8),
|
||||||
pub state: (Header, u8),
|
pub state: (Header, u8),
|
||||||
@@ -623,3 +630,98 @@ impl Columns {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ratatui::widgets::ListItem;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app_data::{ContainerImage, Logs},
|
||||||
|
ui::log_sanitizer,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{ByteStats, ContainerName, CpuStats, LogsTz};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Display CpuStats as a string
|
||||||
|
fn test_container_state_cpustats_to_string() {
|
||||||
|
let test = |f: f64, s: &str| {
|
||||||
|
assert_eq!(CpuStats::new(f).to_string(), s);
|
||||||
|
};
|
||||||
|
|
||||||
|
test(0.0, "00.00%");
|
||||||
|
test(1.5, "01.50%");
|
||||||
|
test(15.15, "15.15%");
|
||||||
|
test(150.15, "150.15%");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Display bytestats as a string, convert into correct data unit (Kb, MB, GB)
|
||||||
|
fn test_container_state_bytestats_to_string() {
|
||||||
|
let test = |u: u64, s: &str| {
|
||||||
|
assert_eq!(ByteStats::new(u).to_string(), s);
|
||||||
|
};
|
||||||
|
|
||||||
|
test(0, "0.00 kB");
|
||||||
|
test(150, "0.15 kB");
|
||||||
|
test(1500, "1.50 kB");
|
||||||
|
test(150_000, "150.00 kB");
|
||||||
|
test(1_500_000, "1.50 MB");
|
||||||
|
test(15_000_000, "15.00 MB");
|
||||||
|
test(150_000_000, "150.00 MB");
|
||||||
|
test(1_500_000_000, "1.50 GB");
|
||||||
|
test(15_000_000_000, "15.00 GB");
|
||||||
|
test(150_000_000_000, "150.00 GB");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// ContainerName as string truncated correctly
|
||||||
|
fn test_container_state_container_name_to_string() {
|
||||||
|
let result = ContainerName::from("name_01");
|
||||||
|
assert_eq!(result.to_string(), "name_01");
|
||||||
|
|
||||||
|
let result = ContainerName::from("name_01_name_01_name_01_name_01_");
|
||||||
|
assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…");
|
||||||
|
|
||||||
|
let result = result.get();
|
||||||
|
assert_eq!(result, "name_01_name_01_name_01_name_01_");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// ContainerImage as string truncated correctly
|
||||||
|
fn test_container_state_container_image() {
|
||||||
|
let result = ContainerImage::from("name_01");
|
||||||
|
assert_eq!(result.to_string(), "name_01");
|
||||||
|
|
||||||
|
let result = ContainerImage::from("name_01_name_01_name_01_name_01_");
|
||||||
|
assert_eq!(result.to_string(), "name_01_name_01_name_01_name_…");
|
||||||
|
|
||||||
|
let result = result.get();
|
||||||
|
assert_eq!(result, "name_01_name_01_name_01_name_01_");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Logs can only contain 1 entry per LogzTz
|
||||||
|
fn test_container_state_logz() {
|
||||||
|
let input = "2023-01-14T19:13:30.783138328Z Lorem ipsum dolor sit amet";
|
||||||
|
let tz = LogsTz::from(input);
|
||||||
|
let mut logs = Logs::default();
|
||||||
|
let line = log_sanitizer::remove_ansi(input);
|
||||||
|
|
||||||
|
logs.insert(ListItem::new(line.clone()), tz.clone());
|
||||||
|
logs.insert(ListItem::new(line.clone()), tz.clone());
|
||||||
|
logs.insert(ListItem::new(line), tz);
|
||||||
|
|
||||||
|
assert_eq!(logs.logs.items.len(), 1);
|
||||||
|
|
||||||
|
let input = "2023-01-15T19:13:30.783138328Z Lorem ipsum dolor sit amet";
|
||||||
|
let tz = LogsTz::from(input);
|
||||||
|
let line = log_sanitizer::remove_ansi(input);
|
||||||
|
|
||||||
|
logs.insert(ListItem::new(line.clone()), tz.clone());
|
||||||
|
logs.insert(ListItem::new(line.clone()), tz.clone());
|
||||||
|
logs.insert(ListItem::new(line), tz);
|
||||||
|
|
||||||
|
assert_eq!(logs.logs.items.len(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1198
-148
File diff suppressed because it is too large
Load Diff
+146
-4
@@ -71,6 +71,7 @@ pub struct DockerData {
|
|||||||
impl DockerData {
|
impl DockerData {
|
||||||
/// Use docker stats to calculate current cpu usage
|
/// Use docker stats to calculate current cpu usage
|
||||||
#[allow(clippy::cast_precision_loss)]
|
#[allow(clippy::cast_precision_loss)]
|
||||||
|
// FIX: this can overflow
|
||||||
fn calculate_usage(stats: &Stats) -> f64 {
|
fn calculate_usage(stats: &Stats) -> f64 {
|
||||||
let mut cpu_percentage = 0.0;
|
let mut cpu_percentage = 0.0;
|
||||||
let previous_cpu = stats.precpu_stats.cpu_usage.total_usage;
|
let previous_cpu = stats.precpu_stats.cpu_usage.total_usage;
|
||||||
@@ -150,7 +151,7 @@ impl DockerData {
|
|||||||
|
|
||||||
app_data
|
app_data
|
||||||
.lock()
|
.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);
|
spawns.lock().remove(&spawn_id);
|
||||||
@@ -162,7 +163,6 @@ impl DockerData {
|
|||||||
/// Update all stats, spawn each container into own tokio::spawn thread
|
/// Update all stats, spawn each container into own tokio::spawn thread
|
||||||
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
|
fn update_all_container_stats(&mut self, all_ids: &[(State, ContainerId)]) {
|
||||||
for (state, id) in all_ids {
|
for (state, id) in all_ids {
|
||||||
// let init = self.init.as_ref().map_or_else(|| None, |x| Some((Arc::clone(x), all_ids.len())));
|
|
||||||
let docker = Arc::clone(&self.docker);
|
let docker = Arc::clone(&self.docker);
|
||||||
let app_data = Arc::clone(&self.app_data);
|
let app_data = Arc::clone(&self.app_data);
|
||||||
let spawns = Arc::clone(&self.spawns);
|
let spawns = Arc::clone(&self.spawns);
|
||||||
@@ -436,7 +436,7 @@ impl DockerData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Send an update message every x ms, where x is the args.docker_interval
|
/// Send an update message every x ms, where x is the args.docker_interval
|
||||||
fn croner(args: &CliArgs, docker_tx: Sender<DockerMessage>) {
|
fn scheduler(args: &CliArgs, docker_tx: Sender<DockerMessage>) {
|
||||||
let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
|
let update_duration = std::time::Duration::from_millis(u64::from(args.docker_interval));
|
||||||
let mut now = std::time::Instant::now();
|
let mut now = std::time::Instant::now();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@@ -472,10 +472,152 @@ impl DockerData {
|
|||||||
spawns: Arc::new(Mutex::new(HashMap::new())),
|
spawns: Arc::new(Mutex::new(HashMap::new())),
|
||||||
};
|
};
|
||||||
inner.initialise_container_data().await;
|
inner.initialise_container_data().await;
|
||||||
Self::croner(&args, docker_tx);
|
Self::scheduler(&args, docker_tx);
|
||||||
inner.message_handler().await;
|
inner.message_handler().await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers
|
// tests, use redis-test container, check logs exists, and selector of logs, and that it increases, and matches end, when you run restart on the docker containers
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use bollard::container::{
|
||||||
|
BlkioStats, CPUStats, CPUUsage, MemoryStats, PidsStats, StorageStats, ThrottlingData,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
fn gen_stats(x: u64, y: u64) -> Stats {
|
||||||
|
Stats {
|
||||||
|
read: String::new(),
|
||||||
|
preread: String::new(),
|
||||||
|
num_procs: 0,
|
||||||
|
pids_stats: PidsStats {
|
||||||
|
current: None,
|
||||||
|
limit: None,
|
||||||
|
},
|
||||||
|
network: None,
|
||||||
|
networks: None,
|
||||||
|
memory_stats: MemoryStats {
|
||||||
|
stats: None,
|
||||||
|
max_usage: None,
|
||||||
|
usage: None,
|
||||||
|
failcnt: None,
|
||||||
|
limit: None,
|
||||||
|
commit: None,
|
||||||
|
commit_peak: None,
|
||||||
|
commitbytes: None,
|
||||||
|
commitpeakbytes: None,
|
||||||
|
privateworkingset: None,
|
||||||
|
},
|
||||||
|
blkio_stats: BlkioStats {
|
||||||
|
io_service_bytes_recursive: None,
|
||||||
|
io_serviced_recursive: None,
|
||||||
|
io_queue_recursive: None,
|
||||||
|
io_service_time_recursive: None,
|
||||||
|
io_wait_time_recursive: None,
|
||||||
|
io_merged_recursive: None,
|
||||||
|
io_time_recursive: None,
|
||||||
|
sectors_recursive: None,
|
||||||
|
},
|
||||||
|
cpu_stats: CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![
|
||||||
|
291_593_800,
|
||||||
|
182_192_900,
|
||||||
|
195_048_700,
|
||||||
|
23_032_300,
|
||||||
|
132_928_700,
|
||||||
|
235_555_600,
|
||||||
|
120_225_700,
|
||||||
|
175_752_000,
|
||||||
|
213_060_300,
|
||||||
|
95_321_600,
|
||||||
|
226_821_000,
|
||||||
|
0,
|
||||||
|
109_151_300,
|
||||||
|
0,
|
||||||
|
86_240_200,
|
||||||
|
1_884_400,
|
||||||
|
59_077_300,
|
||||||
|
23_224_900,
|
||||||
|
95_386_300,
|
||||||
|
144_987_400,
|
||||||
|
]),
|
||||||
|
total_usage: 250_000_000,
|
||||||
|
usage_in_usermode: 1_020_000_000,
|
||||||
|
usage_in_kernelmode: 1_030_000_000,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(x),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
precpu_stats: CPUStats {
|
||||||
|
cpu_usage: CPUUsage {
|
||||||
|
percpu_usage: Some(vec![
|
||||||
|
291_593_800,
|
||||||
|
182_192_900,
|
||||||
|
195_048_700,
|
||||||
|
23_032_300,
|
||||||
|
132_928_700,
|
||||||
|
235_555_600,
|
||||||
|
120_225_700,
|
||||||
|
175_752_000,
|
||||||
|
213_060_300,
|
||||||
|
95_321_600,
|
||||||
|
226_821_000,
|
||||||
|
0,
|
||||||
|
109_151_300,
|
||||||
|
0,
|
||||||
|
86_240_200,
|
||||||
|
1_884_400,
|
||||||
|
59_077_300,
|
||||||
|
23_224_900,
|
||||||
|
93_831_100,
|
||||||
|
144_987_400,
|
||||||
|
]),
|
||||||
|
total_usage: 200_000_000,
|
||||||
|
usage_in_usermode: 1_020_000_000,
|
||||||
|
usage_in_kernelmode: 1_020_000_000,
|
||||||
|
},
|
||||||
|
system_cpu_usage: Some(y),
|
||||||
|
online_cpus: Some(1),
|
||||||
|
throttling_data: ThrottlingData {
|
||||||
|
periods: 0,
|
||||||
|
throttled_periods: 0,
|
||||||
|
throttled_time: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
storage_stats: StorageStats {
|
||||||
|
read_count_normalized: None,
|
||||||
|
read_size_bytes: None,
|
||||||
|
write_count_normalized: None,
|
||||||
|
write_size_bytes: None,
|
||||||
|
},
|
||||||
|
name: "/container_1".to_owned(),
|
||||||
|
id: "1".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(clippy::float_cmp)]
|
||||||
|
/// Test the stats calculator, had to cheat here to get round input/outputs
|
||||||
|
fn test_calculate_usage_no_previous_cpu() {
|
||||||
|
let stats = gen_stats(1_000_000_000, 900_000_000);
|
||||||
|
let result = DockerData::calculate_usage(&stats);
|
||||||
|
assert_eq!(result, 50.0);
|
||||||
|
|
||||||
|
let stats = gen_stats(1_000_000_000, 800_000_000);
|
||||||
|
let result = DockerData::calculate_usage(&stats);
|
||||||
|
assert_eq!(result, 25.0);
|
||||||
|
|
||||||
|
let stats = gen_stats(1_000_000_000, 750_000_000);
|
||||||
|
let result = DockerData::calculate_usage(&stats);
|
||||||
|
assert_eq!(result, 20.00);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -268,11 +268,11 @@ impl InputHandler {
|
|||||||
// This isn't great, just means you can't send docker commands before full initialization of the program
|
// 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();
|
let panel = self.gui_state.lock().get_selected_panel();
|
||||||
if panel == SelectablePanel::Commands {
|
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 {
|
if let Some(command) = option_command {
|
||||||
// Poor way of disallowing commands to be sent to a containerised okxer
|
// 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;
|
return;
|
||||||
};
|
};
|
||||||
let option_id = self.app_data.lock().get_selected_container_id();
|
let option_id = self.app_data.lock().get_selected_container_id();
|
||||||
@@ -337,7 +337,7 @@ impl InputHandler {
|
|||||||
match selected_panel {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_start(),
|
SelectablePanel::Containers => locked_data.containers_start(),
|
||||||
SelectablePanel::Logs => locked_data.log_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 {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_end(),
|
SelectablePanel::Containers => locked_data.containers_end(),
|
||||||
SelectablePanel::Logs => locked_data.log_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 {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_next(),
|
SelectablePanel::Containers => locked_data.containers_next(),
|
||||||
SelectablePanel::Logs => locked_data.log_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 {
|
match selected_panel {
|
||||||
SelectablePanel::Containers => locked_data.containers_previous(),
|
SelectablePanel::Containers => locked_data.containers_previous(),
|
||||||
SelectablePanel::Logs => locked_data.log_previous(),
|
SelectablePanel::Logs => locked_data.log_previous(),
|
||||||
SelectablePanel::Commands => locked_data.docker_command_previous(),
|
SelectablePanel::Commands => locked_data.docker_controls_previous(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+79
@@ -164,3 +164,82 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::unwrap_used, clippy::many_single_char_names, unused)]
|
||||||
|
mod tests {
|
||||||
|
use bollard::service::ContainerSummary;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app_data::{AppData, ContainerId, ContainerItem, State, StatefulList},
|
||||||
|
parse_args::CliArgs,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const fn gen_args() -> CliArgs {
|
||||||
|
CliArgs {
|
||||||
|
color: false,
|
||||||
|
docker_interval: 1000,
|
||||||
|
gui: true,
|
||||||
|
host: None,
|
||||||
|
in_container: false,
|
||||||
|
save_dir: None,
|
||||||
|
raw: false,
|
||||||
|
show_self: false,
|
||||||
|
timestamp: false,
|
||||||
|
use_cli: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_item(id: &ContainerId, index: usize) -> ContainerItem {
|
||||||
|
ContainerItem::new(
|
||||||
|
u64::try_from(index).unwrap(),
|
||||||
|
id.clone(),
|
||||||
|
format!("image_{index}"),
|
||||||
|
false,
|
||||||
|
format!("container_{index}"),
|
||||||
|
State::Running,
|
||||||
|
format!("Up {index} hour"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_appdata(containers: &[ContainerItem]) -> AppData {
|
||||||
|
AppData {
|
||||||
|
containers: StatefulList::new(containers.to_vec()),
|
||||||
|
error: None,
|
||||||
|
sorted_by: None,
|
||||||
|
args: gen_args(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_containers() -> (Vec<ContainerId>, Vec<ContainerItem>) {
|
||||||
|
let ids = (1..=3)
|
||||||
|
.map(|i| ContainerId::from(format!("{i}").as_str()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let containers = ids
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, id)| gen_item(id, index + 1))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
(ids, containers)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gen_container_summary(index: usize, state: &str) -> ContainerSummary {
|
||||||
|
ContainerSummary {
|
||||||
|
id: Some(format!("{index}")),
|
||||||
|
names: Some(vec![format!("container_{}", index)]),
|
||||||
|
image: Some(format!("image_{index}")),
|
||||||
|
image_id: Some(format!("{index}")),
|
||||||
|
command: None,
|
||||||
|
created: Some(i64::try_from(index).unwrap()),
|
||||||
|
ports: None,
|
||||||
|
size_rw: None,
|
||||||
|
size_root_fs: None,
|
||||||
|
labels: None,
|
||||||
|
state: Some(state.to_owned()),
|
||||||
|
status: Some(format!("Up {index} hour")),
|
||||||
|
host_config: None,
|
||||||
|
network_settings: None,
|
||||||
|
mounts: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -72,3 +72,79 @@ pub mod log_sanitizer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ratatui::{
|
||||||
|
style::{Color, Style},
|
||||||
|
text::{Line, Span},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::log_sanitizer;
|
||||||
|
|
||||||
|
// This spells out "oxker", with each char having a foreground and background colour
|
||||||
|
const INPUT: &str = "\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m";
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
/// Return test raw, as in show escape codes
|
||||||
|
fn color_match_raw() {
|
||||||
|
let result = log_sanitizer::raw(INPUT);
|
||||||
|
let expected = vec![Line {
|
||||||
|
spans: [Span {
|
||||||
|
content: std::borrow::Cow::Borrowed(
|
||||||
|
"\x1b[31;47mo\x1b[32;40mx\x1b[33;41mk\x1b[34;42me\x1b[35;43mr\x1b[0m",
|
||||||
|
),
|
||||||
|
style: Style::default(),
|
||||||
|
}]
|
||||||
|
.to_vec(),
|
||||||
|
alignment: None,
|
||||||
|
}];
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Use the escape codes to colorize the text
|
||||||
|
fn color_match_colorize() {
|
||||||
|
let result = log_sanitizer::colorize_logs(INPUT);
|
||||||
|
let expected = vec![Line {
|
||||||
|
spans: vec![
|
||||||
|
Span {
|
||||||
|
content: std::borrow::Cow::Borrowed("o"),
|
||||||
|
style: Style::default().fg(Color::Red).bg(Color::White),
|
||||||
|
},
|
||||||
|
Span {
|
||||||
|
content: std::borrow::Cow::Borrowed("x"),
|
||||||
|
style: Style::default().fg(Color::Green).bg(Color::Black),
|
||||||
|
},
|
||||||
|
Span {
|
||||||
|
content: std::borrow::Cow::Borrowed("k"),
|
||||||
|
style: Style::default().fg(Color::Yellow).bg(Color::Red),
|
||||||
|
},
|
||||||
|
Span {
|
||||||
|
content: std::borrow::Cow::Borrowed("e"),
|
||||||
|
style: Style::default().fg(Color::Blue).bg(Color::Green),
|
||||||
|
},
|
||||||
|
Span {
|
||||||
|
content: std::borrow::Cow::Borrowed("r"),
|
||||||
|
style: Style::default().fg(Color::Magenta).bg(Color::Yellow),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
alignment: None,
|
||||||
|
}];
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
// Remove all escape ansi codes from given input
|
||||||
|
fn color_match_remove_ansi() {
|
||||||
|
let result = log_sanitizer::remove_ansi(INPUT);
|
||||||
|
let expected = vec![Line {
|
||||||
|
spans: vec![Span {
|
||||||
|
content: std::borrow::Cow::Borrowed("oxker"),
|
||||||
|
style: Style::default(),
|
||||||
|
}],
|
||||||
|
alignment: None,
|
||||||
|
}];
|
||||||
|
assert_eq!(result, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1817
-37
File diff suppressed because it is too large
Load Diff
+7
-28
@@ -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
|
/// Frequent data required by multiple framde drawing functions, can reduce mutex reads by placing it all in here
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct FrameData {
|
pub struct FrameData {
|
||||||
@@ -279,21 +263,16 @@ impl From<(MutexGuard<'_, AppData>, MutexGuard<'_, GuiState>)> for FrameData {
|
|||||||
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
|
fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mutex<GuiState>>) {
|
||||||
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
|
let fd = FrameData::from((app_data.lock(), gui_state.lock()));
|
||||||
|
|
||||||
let whole_layout = get_wholelayout(f);
|
let whole_layout = Layout::default()
|
||||||
#[cfg(debug_assertions)]
|
.direction(Direction::Vertical)
|
||||||
draw_blocks::debug_bar(whole_layout[0], f, app_data.lock().get_debug_string());
|
.constraints([Constraint::Min(1), Constraint::Min(100)].as_ref())
|
||||||
|
.split(f.size());
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
let whole_layout_split = (1, 2);
|
|
||||||
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
let whole_layout_split = (0, 1);
|
|
||||||
|
|
||||||
// Split into 3, containers+controls, logs, then graphs
|
// Split into 3, containers+controls, logs, then graphs
|
||||||
let upper_main = Layout::default()
|
let upper_main = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([Constraint::Max(fd.height), Constraint::Percentage(50)].as_ref())
|
.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 {
|
let top_split = if fd.has_containers {
|
||||||
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
|
vec![Constraint::Percentage(90), Constraint::Percentage(10)]
|
||||||
@@ -318,11 +297,11 @@ fn draw_frame(f: &mut Frame, app_data: &Arc<Mutex<AppData>>, gui_state: &Arc<Mut
|
|||||||
.constraints(lower_split)
|
.constraints(lower_split)
|
||||||
.split(upper_main[1]);
|
.split(upper_main[1]);
|
||||||
|
|
||||||
draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state, &fd.columns);
|
draw_blocks::containers(app_data, top_panel[0], f, &fd, gui_state);
|
||||||
|
|
||||||
draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state);
|
draw_blocks::logs(app_data, lower_main[0], f, &fd, gui_state);
|
||||||
|
|
||||||
draw_blocks::heading_bar(whole_layout[whole_layout_split.0], f, &fd, gui_state);
|
draw_blocks::heading_bar(whole_layout[0], f, &fd, gui_state);
|
||||||
|
|
||||||
if let Some(id) = fd.delete_confirm.as_ref() {
|
if let Some(id) = fd.delete_confirm.as_ref() {
|
||||||
app_data.lock().get_container_name_by_id(id).map_or_else(
|
app_data.lock().get_container_name_by_id(id).map_or_else(
|
||||||
|
|||||||
Reference in New Issue
Block a user