use crate::theme::Gradient;
use presentar_core::{
Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
LayoutResult, Point, Rect, Size, TextStyle, TypeId, Widget,
};
use std::any::Any;
use std::cmp::Ordering;
use std::fmt::Write as _;
use std::time::Duration;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ProcessState {
Running,
#[default]
Sleeping,
DiskWait,
Zombie,
Stopped,
Idle,
}
impl ProcessState {
#[must_use]
pub fn char(&self) -> char {
match self {
Self::Running => 'R',
Self::Sleeping => 'S',
Self::DiskWait => 'D',
Self::Zombie => 'Z',
Self::Stopped => 'T',
Self::Idle => 'I',
}
}
#[must_use]
pub fn color(&self) -> Color {
match self {
Self::Running => Color::new(0.3, 0.9, 0.3, 1.0), Self::Sleeping => Color::new(0.5, 0.5, 0.5, 1.0), Self::DiskWait => Color::new(1.0, 0.7, 0.2, 1.0), Self::Zombie => Color::new(1.0, 0.3, 0.3, 1.0), Self::Stopped => Color::new(0.9, 0.9, 0.3, 1.0), Self::Idle => Color::new(0.4, 0.4, 0.4, 1.0), }
}
}
#[derive(Debug, Clone)]
pub struct ProcessEntry {
pub pid: u32,
pub user: String,
pub cpu_percent: f32,
pub mem_percent: f32,
pub command: String,
pub cmdline: Option<String>,
pub state: ProcessState,
pub oom_score: Option<i32>,
pub cgroup: Option<String>,
pub nice: Option<i32>,
pub threads: Option<u32>,
pub parent_pid: Option<u32>,
pub tree_depth: usize,
pub is_last_child: bool,
pub tree_prefix: String,
}
impl ProcessEntry {
#[must_use]
pub fn new(
pid: u32,
user: impl Into<String>,
cpu: f32,
mem: f32,
command: impl Into<String>,
) -> Self {
Self {
pid,
user: user.into(),
cpu_percent: cpu,
mem_percent: mem,
command: command.into(),
cmdline: None,
state: ProcessState::default(),
oom_score: None,
cgroup: None,
nice: None,
threads: None,
parent_pid: None,
tree_depth: 0,
is_last_child: false,
tree_prefix: String::new(),
}
}
#[must_use]
pub fn with_cmdline(mut self, cmdline: impl Into<String>) -> Self {
self.cmdline = Some(cmdline.into());
self
}
#[must_use]
pub fn with_state(mut self, state: ProcessState) -> Self {
self.state = state;
self
}
#[must_use]
pub fn with_oom_score(mut self, score: i32) -> Self {
self.oom_score = Some(score);
self
}
#[must_use]
pub fn with_cgroup(mut self, cgroup: impl Into<String>) -> Self {
self.cgroup = Some(cgroup.into());
self
}
#[must_use]
pub fn with_nice(mut self, nice: i32) -> Self {
self.nice = Some(nice);
self
}
#[must_use]
pub fn with_threads(mut self, threads: u32) -> Self {
self.threads = Some(threads);
self
}
#[must_use]
pub fn with_parent_pid(mut self, ppid: u32) -> Self {
self.parent_pid = Some(ppid);
self
}
pub fn set_tree_info(&mut self, depth: usize, is_last: bool, prefix: String) {
self.tree_depth = depth;
self.is_last_child = is_last;
self.tree_prefix = prefix;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessSort {
Pid,
User,
Cpu,
Memory,
Command,
Oom,
}
#[derive(Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct ProcessTable {
processes: Vec<ProcessEntry>,
selected: usize,
scroll_offset: usize,
sort_by: ProcessSort,
sort_ascending: bool,
cpu_gradient: Gradient,
mem_gradient: Gradient,
show_cmdline: bool,
compact: bool,
show_oom: bool,
show_nice: bool,
show_threads: bool,
tree_view: bool,
bounds: Rect,
}
impl Default for ProcessTable {
fn default() -> Self {
Self::new()
}
}
impl ProcessTable {
#[must_use]
pub fn new() -> Self {
Self {
processes: Vec::new(),
selected: 0,
scroll_offset: 0,
sort_by: ProcessSort::Cpu,
sort_ascending: false, cpu_gradient: Gradient::from_hex(&["#7aa2f7", "#e0af68", "#f7768e"]),
mem_gradient: Gradient::from_hex(&["#9ece6a", "#e0af68", "#f7768e"]),
show_cmdline: false,
compact: false,
show_oom: false,
show_nice: false,
show_threads: false,
tree_view: false,
bounds: Rect::default(),
}
}
pub fn set_processes(&mut self, processes: Vec<ProcessEntry>) {
self.processes = processes;
if self.tree_view {
self.build_tree();
} else {
self.sort_processes();
}
if !self.processes.is_empty() && self.selected >= self.processes.len() {
self.selected = self.processes.len() - 1;
}
}
pub fn add_process(&mut self, process: ProcessEntry) {
self.processes.push(process);
}
pub fn clear(&mut self) {
self.processes.clear();
self.selected = 0;
self.scroll_offset = 0;
}
#[must_use]
pub fn with_cpu_gradient(mut self, gradient: Gradient) -> Self {
self.cpu_gradient = gradient;
self
}
#[must_use]
pub fn with_mem_gradient(mut self, gradient: Gradient) -> Self {
self.mem_gradient = gradient;
self
}
#[must_use]
pub fn compact(mut self) -> Self {
self.compact = true;
self
}
#[must_use]
pub fn with_cmdline(mut self) -> Self {
self.show_cmdline = true;
self
}
#[must_use]
pub fn with_oom(mut self) -> Self {
self.show_oom = true;
self
}
#[must_use]
pub fn with_nice_column(mut self) -> Self {
self.show_nice = true;
self
}
#[must_use]
pub fn with_threads_column(mut self) -> Self {
self.show_threads = true;
self
}
#[must_use]
pub fn with_tree_view(mut self) -> Self {
self.tree_view = true;
self
}
pub fn toggle_tree_view(&mut self) {
self.tree_view = !self.tree_view;
if self.tree_view {
self.build_tree();
}
}
#[must_use]
pub fn is_tree_view(&self) -> bool {
self.tree_view
}
pub fn sort_by(&mut self, column: ProcessSort) {
if self.sort_by == column {
self.sort_ascending = !self.sort_ascending;
} else {
self.sort_by = column;
self.sort_ascending = !matches!(
column,
ProcessSort::Cpu | ProcessSort::Memory | ProcessSort::Oom
);
}
self.sort_processes();
}
#[must_use]
pub fn current_sort(&self) -> ProcessSort {
self.sort_by
}
#[must_use]
pub fn selected(&self) -> usize {
self.selected
}
#[must_use]
pub fn selected_process(&self) -> Option<&ProcessEntry> {
self.processes.get(self.selected)
}
pub fn select(&mut self, row: usize) {
if !self.processes.is_empty() {
self.selected = row.min(self.processes.len() - 1);
self.ensure_visible();
}
}
pub fn select_prev(&mut self) {
if self.selected > 0 {
self.selected -= 1;
self.ensure_visible();
}
}
pub fn select_next(&mut self) {
if !self.processes.is_empty() && self.selected < self.processes.len() - 1 {
self.selected += 1;
self.ensure_visible();
}
}
#[must_use]
pub fn len(&self) -> usize {
self.processes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.processes.is_empty()
}
fn build_tree(&mut self) {
use std::collections::HashMap;
if self.processes.is_empty() {
return;
}
let pid_to_idx: HashMap<u32, usize> = self
.processes
.iter()
.enumerate()
.map(|(i, p)| (p.pid, i))
.collect();
let mut children: HashMap<u32, Vec<usize>> = HashMap::new();
let mut roots: Vec<usize> = Vec::new();
for (idx, proc) in self.processes.iter().enumerate() {
if let Some(ppid) = proc.parent_pid {
if pid_to_idx.contains_key(&ppid) {
children.entry(ppid).or_default().push(idx);
} else {
roots.push(idx);
}
} else {
roots.push(idx);
}
}
for children_list in children.values_mut() {
children_list.sort_by(|&a, &b| {
self.processes[b]
.cpu_percent
.partial_cmp(&self.processes[a].cpu_percent)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
roots.sort_by(|&a, &b| {
self.processes[b]
.cpu_percent
.partial_cmp(&self.processes[a].cpu_percent)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut tree_order: Vec<(usize, usize, bool, String)> = Vec::new();
let roots_len = roots.len();
for (i, &root_idx) in roots.iter().enumerate() {
let is_last = i == roots_len - 1;
Self::build_tree_dfs(
root_idx,
0,
"",
is_last,
&self.processes,
&children,
&mut tree_order,
);
}
let old_processes = std::mem::take(&mut self.processes);
self.processes.reserve(tree_order.len());
for (idx, depth, is_last, prefix) in tree_order {
let mut proc = old_processes[idx].clone();
proc.set_tree_info(depth, is_last, prefix);
self.processes.push(proc);
}
}
fn build_tree_dfs(
idx: usize,
depth: usize,
prefix: &str,
is_last: bool,
processes: &[ProcessEntry],
children: &std::collections::HashMap<u32, Vec<usize>>,
tree_order: &mut Vec<(usize, usize, bool, String)>,
) {
let proc = &processes[idx];
let current_prefix = if depth == 0 {
String::new()
} else if is_last {
format!("{prefix}└─")
} else {
format!("{prefix}├─")
};
tree_order.push((idx, depth, is_last, current_prefix));
let next_prefix = if depth == 0 {
String::new()
} else if is_last {
format!("{prefix} ")
} else {
format!("{prefix}│ ")
};
if let Some(child_indices) = children.get(&proc.pid) {
let len = child_indices.len();
for (i, &child_idx) in child_indices.iter().enumerate() {
let child_is_last = i == len - 1;
Self::build_tree_dfs(
child_idx,
depth + 1,
&next_prefix,
child_is_last,
processes,
children,
tree_order,
);
}
}
}
fn sort_processes(&mut self) {
let ascending = self.sort_ascending;
match self.sort_by {
ProcessSort::Pid => {
self.processes.sort_by(|a, b| {
if ascending {
a.pid.cmp(&b.pid)
} else {
b.pid.cmp(&a.pid)
}
});
}
ProcessSort::User => {
self.processes.sort_by(|a, b| {
if ascending {
a.user.cmp(&b.user)
} else {
b.user.cmp(&a.user)
}
});
}
ProcessSort::Cpu => {
self.processes.sort_by(|a, b| {
let cmp = a
.cpu_percent
.partial_cmp(&b.cpu_percent)
.unwrap_or(std::cmp::Ordering::Equal);
if ascending {
cmp
} else {
cmp.reverse()
}
});
}
ProcessSort::Memory => {
self.processes.sort_by(|a, b| {
let cmp = a
.mem_percent
.partial_cmp(&b.mem_percent)
.unwrap_or(std::cmp::Ordering::Equal);
if ascending {
cmp
} else {
cmp.reverse()
}
});
}
ProcessSort::Command => {
self.processes.sort_by(|a, b| {
if ascending {
a.command.cmp(&b.command)
} else {
b.command.cmp(&a.command)
}
});
}
ProcessSort::Oom => {
self.processes.sort_by(|a, b| {
let a_oom = a.oom_score.unwrap_or(0);
let b_oom = b.oom_score.unwrap_or(0);
if ascending {
a_oom.cmp(&b_oom)
} else {
b_oom.cmp(&a_oom)
}
});
}
}
}
fn ensure_visible(&mut self) {
let visible_rows = (self.bounds.height as usize).saturating_sub(2);
if visible_rows == 0 {
return;
}
if self.selected < self.scroll_offset {
self.scroll_offset = self.selected;
} else if self.selected >= self.scroll_offset + visible_rows {
self.scroll_offset = self.selected - visible_rows + 1;
}
}
fn truncate(s: &str, width: usize) -> String {
if s.chars().count() <= width {
format!("{s:width$}")
} else if width > 1 {
let chars: String = s.chars().take(width - 1).collect();
format!("{chars}…")
} else {
s.chars().take(width).collect()
}
}
fn oom_color(oom: i32) -> Color {
if oom > 500 {
Color::new(1.0, 0.3, 0.3, 1.0) } else if oom > 200 {
Color::new(1.0, 0.8, 0.2, 1.0) } else {
Color::new(0.5, 0.8, 0.5, 1.0) }
}
fn nice_color(ni: i32) -> Color {
match ni.cmp(&0) {
Ordering::Less => Color::new(0.3, 0.9, 0.9, 1.0), Ordering::Greater => Color::new(0.6, 0.6, 0.6, 1.0), Ordering::Equal => Color::new(0.8, 0.8, 0.8, 1.0), }
}
fn threads_color(th: u32) -> Color {
if th > 50 {
Color::new(0.3, 0.9, 0.9, 1.0) } else if th > 10 {
Color::new(1.0, 0.8, 0.2, 1.0) } else {
Color::new(0.8, 0.8, 0.8, 1.0) }
}
fn build_header(&self, cols: &ColumnWidths) -> String {
let sep = if self.compact { " " } else { " │ " };
let mut header = String::new();
let _ = write!(header, "{:>w$}", "PID", w = cols.pid);
if self.compact {
header.push(' ');
let _ = write!(header, "{:>1}", "S");
} else {
header.push_str(sep);
let _ = write!(header, "{:w$}", "USER", w = cols.user);
}
if self.show_oom {
header.push_str(sep);
let _ = write!(header, "{:>3}", "OOM");
}
if self.show_nice {
header.push_str(sep);
let _ = write!(header, "{:>3}", "NI");
}
if self.show_threads {
header.push_str(sep);
let _ = write!(header, "{:>3}", "TH");
}
header.push_str(sep);
let _ = write!(
header,
"{:>w$}",
if self.compact { "C%" } else { "CPU%" },
w = cols.cpu
);
header.push_str(sep);
let _ = write!(
header,
"{:>w$}",
if self.compact { "M%" } else { "MEM%" },
w = cols.mem
);
header.push_str(sep);
let _ = write!(header, "{:w$}", "COMMAND", w = cols.cmd);
header
}
#[allow(clippy::too_many_arguments)]
fn draw_row(
&self,
canvas: &mut dyn Canvas,
proc: &ProcessEntry,
y: f32,
is_selected: bool,
cols: &ColumnWidths,
default_style: &TextStyle,
) {
let sep = if self.compact { 1.0 } else { 3.0 };
let mut x = self.bounds.x;
canvas.draw_text(
&format!("{:>w$}", proc.pid, w = cols.pid),
Point::new(x, y),
default_style,
);
x += cols.pid as f32;
if self.compact {
x += 1.0;
canvas.draw_text(
&proc.state.char().to_string(),
Point::new(x, y),
&TextStyle {
color: proc.state.color(),
..Default::default()
},
);
x += 1.0;
} else {
x += sep;
canvas.draw_text(
&Self::truncate(&proc.user, cols.user),
Point::new(x, y),
default_style,
);
x += cols.user as f32;
}
if self.show_oom {
x += sep;
let oom = proc.oom_score.unwrap_or(0);
canvas.draw_text(
&format!("{oom:>3}"),
Point::new(x, y),
&TextStyle {
color: Self::oom_color(oom),
..Default::default()
},
);
x += 3.0;
}
if self.show_nice {
x += sep;
let ni = proc.nice.unwrap_or(0);
canvas.draw_text(
&format!("{ni:>3}"),
Point::new(x, y),
&TextStyle {
color: Self::nice_color(ni),
..Default::default()
},
);
x += 3.0;
}
if self.show_threads {
x += sep;
let th = proc.threads.unwrap_or(1);
canvas.draw_text(
&format!("{th:>3}"),
Point::new(x, y),
&TextStyle {
color: Self::threads_color(th),
..Default::default()
},
);
x += 3.0;
}
x += sep;
canvas.draw_text(
&format!("{:>5.1}%", proc.cpu_percent),
Point::new(x, y),
&TextStyle {
color: self.cpu_gradient.for_percent(proc.cpu_percent as f64),
..Default::default()
},
);
x += cols.cpu as f32;
x += sep;
canvas.draw_text(
&format!("{:>5.1}%", proc.mem_percent),
Point::new(x, y),
&TextStyle {
color: self.mem_gradient.for_percent(proc.mem_percent as f64),
..Default::default()
},
);
x += cols.mem as f32;
x += sep;
self.draw_command(canvas, proc, x, y, is_selected, cols.cmd, default_style);
}
#[allow(clippy::too_many_arguments)]
fn draw_command(
&self,
canvas: &mut dyn Canvas,
proc: &ProcessEntry,
x: f32,
y: f32,
is_selected: bool,
cmd_w: usize,
default_style: &TextStyle,
) {
let cmd = if self.show_cmdline {
proc.cmdline.as_deref().unwrap_or(&proc.command)
} else {
&proc.command
};
let cmd_style = if is_selected {
TextStyle {
color: Color::new(1.0, 1.0, 1.0, 1.0),
..Default::default()
}
} else {
default_style.clone()
};
if self.tree_view && !proc.tree_prefix.is_empty() {
let prefix_len = proc.tree_prefix.chars().count();
canvas.draw_text(
&proc.tree_prefix,
Point::new(x, y),
&TextStyle {
color: Color::new(0.4, 0.5, 0.6, 1.0),
..Default::default()
},
);
canvas.draw_text(
&Self::truncate(cmd, cmd_w.saturating_sub(prefix_len)),
Point::new(x + prefix_len as f32, y),
&cmd_style,
);
} else {
canvas.draw_text(&Self::truncate(cmd, cmd_w), Point::new(x, y), &cmd_style);
}
}
}
#[allow(dead_code)]
struct ColumnWidths {
pid: usize,
state: usize,
oom: usize,
nice: usize,
threads: usize,
user: usize,
cpu: usize,
mem: usize,
sep: usize,
cmd: usize,
num_seps: usize,
}
impl ColumnWidths {
fn new(table: &ProcessTable, width: usize) -> Self {
let pid = 7;
let state = if table.compact { 2 } else { 0 };
let oom = if table.show_oom { 4 } else { 0 };
let nice = if table.show_nice { 4 } else { 0 };
let threads = if table.show_threads { 4 } else { 0 };
let user = if table.compact { 0 } else { 8 };
let cpu = 6;
let mem = 6;
let sep = if table.compact { 1 } else { 3 };
let extra_cols = usize::from(table.show_oom)
+ usize::from(table.show_nice)
+ usize::from(table.show_threads);
let num_seps = if table.compact { 3 } else { 4 } + extra_cols;
let fixed = pid + state + oom + nice + threads + user + cpu + mem + sep * num_seps;
let cmd = width.saturating_sub(fixed);
Self {
pid,
state,
oom,
nice,
threads,
user,
cpu,
mem,
sep,
cmd,
num_seps,
}
}
}
impl Brick for ProcessTable {
fn brick_name(&self) -> &'static str {
"process_table"
}
fn assertions(&self) -> &[BrickAssertion] {
static ASSERTIONS: &[BrickAssertion] = &[BrickAssertion::max_latency_ms(16)];
ASSERTIONS
}
fn budget(&self) -> BrickBudget {
BrickBudget::uniform(16)
}
fn verify(&self) -> BrickVerification {
let passed = if self.processes.is_empty() || self.selected < self.processes.len() {
vec![BrickAssertion::max_latency_ms(16)]
} else {
vec![]
};
let failed = if !self.processes.is_empty() && self.selected >= self.processes.len() {
vec![(
BrickAssertion::max_latency_ms(16),
format!(
"Selected {} >= process count {}",
self.selected,
self.processes.len()
),
)]
} else {
vec![]
};
BrickVerification {
passed,
failed,
verification_time: Duration::from_micros(10),
}
}
fn to_html(&self) -> String {
String::new()
}
fn to_css(&self) -> String {
String::new()
}
}
impl Widget for ProcessTable {
fn type_id(&self) -> TypeId {
TypeId::of::<Self>()
}
fn measure(&self, constraints: Constraints) -> Size {
let min_width = if self.compact { 40.0 } else { 60.0 };
let width = constraints.max_width.max(min_width);
let height = (self.processes.len() + 2).min(30) as f32;
constraints.constrain(Size::new(width, height.max(3.0)))
}
fn layout(&mut self, bounds: Rect) -> LayoutResult {
self.bounds = bounds;
self.ensure_visible();
LayoutResult {
size: Size::new(bounds.width, bounds.height),
}
}
fn paint(&self, canvas: &mut dyn Canvas) {
let width = self.bounds.width as usize;
let height = self.bounds.height as usize;
if width == 0 || height == 0 {
return;
}
let cols = ColumnWidths::new(self, width);
let header_style = TextStyle {
color: Color::new(0.0, 1.0, 1.0, 1.0),
weight: presentar_core::FontWeight::Bold,
..Default::default()
};
canvas.draw_text(
&self.build_header(&cols),
Point::new(self.bounds.x, self.bounds.y),
&header_style,
);
if height > 1 {
canvas.draw_text(
&"─".repeat(width),
Point::new(self.bounds.x, self.bounds.y + 1.0),
&TextStyle {
color: Color::new(0.3, 0.3, 0.4, 1.0),
..Default::default()
},
);
}
let default_style = TextStyle {
color: Color::new(0.8, 0.8, 0.8, 1.0),
..Default::default()
};
let visible_rows = height.saturating_sub(2);
for (i, proc_idx) in (self.scroll_offset..self.processes.len())
.take(visible_rows)
.enumerate()
{
let proc = &self.processes[proc_idx];
let y = self.bounds.y + 2.0 + i as f32;
let is_selected = proc_idx == self.selected;
if is_selected {
canvas.fill_rect(
Rect::new(self.bounds.x, y, self.bounds.width, 1.0),
Color::new(0.2, 0.2, 0.4, 0.5),
);
}
self.draw_row(canvas, proc, y, is_selected, &cols, &default_style);
}
if self.processes.is_empty() && height > 2 {
canvas.draw_text(
"No processes",
Point::new(self.bounds.x + 1.0, self.bounds.y + 2.0),
&TextStyle {
color: Color::new(0.5, 0.5, 0.5, 1.0),
..Default::default()
},
);
}
}
fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
match event {
Event::KeyDown { key } => {
match key {
Key::Up | Key::K => self.select_prev(),
Key::Down | Key::J => self.select_next(),
Key::C => self.sort_by(ProcessSort::Cpu),
Key::M => self.sort_by(ProcessSort::Memory),
Key::P => self.sort_by(ProcessSort::Pid),
Key::N => self.sort_by(ProcessSort::Command),
Key::O => self.sort_by(ProcessSort::Oom),
Key::T => self.toggle_tree_view(), _ => {}
}
None
}
_ => None,
}
}
fn children(&self) -> &[Box<dyn Widget>] {
&[]
}
fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
&mut []
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_processes() -> Vec<ProcessEntry> {
vec![
ProcessEntry::new(1, "root", 0.5, 0.1, "systemd"),
ProcessEntry::new(1234, "noah", 25.0, 5.5, "firefox"),
ProcessEntry::new(5678, "noah", 80.0, 12.3, "rustc"),
]
}
#[test]
fn test_process_table_new() {
let table = ProcessTable::new();
assert!(table.is_empty());
}
#[test]
fn test_process_table_set_processes() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
assert_eq!(table.len(), 3);
}
#[test]
fn test_process_table_add_process() {
let mut table = ProcessTable::new();
table.add_process(ProcessEntry::new(1, "root", 0.0, 0.0, "init"));
assert_eq!(table.len(), 1);
}
#[test]
fn test_process_table_clear() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.select(1);
table.clear();
assert!(table.is_empty());
assert_eq!(table.selected(), 0);
}
#[test]
fn test_process_table_selection() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
assert_eq!(table.selected(), 0);
table.select_next();
assert_eq!(table.selected(), 1);
table.select_prev();
assert_eq!(table.selected(), 0);
}
#[test]
fn test_process_table_select_bounds() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.select(100);
assert_eq!(table.selected(), 2);
table.select_prev();
table.select_prev();
table.select_prev();
assert_eq!(table.selected(), 0);
}
#[test]
fn test_process_table_sort_cpu() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
assert_eq!(table.processes[0].command, "rustc");
}
#[test]
fn test_process_table_sort_toggle() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.sort_by(ProcessSort::Cpu); assert_eq!(table.processes[0].command, "systemd");
}
#[test]
fn test_process_table_sort_pid() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.sort_by(ProcessSort::Pid);
assert_eq!(table.processes[0].pid, 1);
}
#[test]
fn test_process_table_sort_memory() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.sort_by(ProcessSort::Memory);
assert_eq!(table.processes[0].command, "rustc");
}
#[test]
fn test_process_table_selected_process() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
let proc = table.selected_process().unwrap();
assert_eq!(proc.command, "rustc"); }
#[test]
fn test_process_entry_with_cmdline() {
let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "bash").with_cmdline("/bin/bash --login");
assert_eq!(proc.cmdline.as_deref(), Some("/bin/bash --login"));
}
#[test]
fn test_process_table_compact() {
let table = ProcessTable::new().compact();
assert!(table.compact);
}
#[test]
fn test_process_table_with_cmdline() {
let table = ProcessTable::new().with_cmdline();
assert!(table.show_cmdline);
}
#[test]
fn test_process_table_verify() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
assert!(table.verify().is_valid());
}
#[test]
fn test_process_table_verify_invalid() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.selected = 100;
assert!(!table.verify().is_valid());
}
#[test]
fn test_process_table_measure() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
let size = table.measure(Constraints::new(0.0, 100.0, 0.0, 50.0));
assert!(size.width >= 60.0);
assert!(size.height >= 3.0);
}
#[test]
fn test_process_table_layout() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
let result = table.layout(Rect::new(0.0, 0.0, 80.0, 20.0));
assert_eq!(result.size.width, 80.0);
}
#[test]
fn test_process_table_event_keys() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.event(&Event::KeyDown { key: Key::J });
assert_eq!(table.selected(), 1);
table.event(&Event::KeyDown { key: Key::K });
assert_eq!(table.selected(), 0);
table.event(&Event::KeyDown { key: Key::P });
assert_eq!(table.current_sort(), ProcessSort::Pid);
}
#[test]
fn test_process_table_brick_name() {
let table = ProcessTable::new();
assert_eq!(table.brick_name(), "process_table");
}
#[test]
fn test_process_table_default() {
let table = ProcessTable::default();
assert!(table.is_empty());
}
#[test]
fn test_process_table_children() {
let table = ProcessTable::new();
assert!(table.children().is_empty());
}
#[test]
fn test_process_table_children_mut() {
let mut table = ProcessTable::new();
assert!(table.children_mut().is_empty());
}
#[test]
fn test_process_table_truncate() {
assert_eq!(ProcessTable::truncate("hello", 10), "hello ");
assert_eq!(ProcessTable::truncate("hello world", 8), "hello w…");
assert_eq!(ProcessTable::truncate("hi", 2), "hi");
assert!(ProcessTable::truncate("long text here", 6).ends_with('…'));
}
#[test]
fn test_process_table_type_id() {
let table = ProcessTable::new();
assert_eq!(Widget::type_id(&table), TypeId::of::<ProcessTable>());
}
#[test]
fn test_process_table_to_html() {
let table = ProcessTable::new();
assert!(table.to_html().is_empty());
}
#[test]
fn test_process_table_to_css() {
let table = ProcessTable::new();
assert!(table.to_css().is_empty());
}
#[test]
fn test_process_table_sort_command() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.sort_by(ProcessSort::Command);
assert_eq!(table.processes[0].command, "firefox");
}
#[test]
fn test_process_table_sort_user() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.sort_by(ProcessSort::User);
assert_eq!(table.processes[0].user, "noah");
}
#[test]
fn test_process_table_sort_oom() {
let mut table = ProcessTable::new();
let entries = vec![
ProcessEntry::new(1, "user", 10.0, 5.0, "low_oom").with_oom_score(100),
ProcessEntry::new(2, "user", 10.0, 5.0, "high_oom").with_oom_score(800),
ProcessEntry::new(3, "user", 10.0, 5.0, "med_oom").with_oom_score(400),
];
table.set_processes(entries);
table.sort_by(ProcessSort::Oom);
assert_eq!(table.processes[0].command, "high_oom");
assert_eq!(table.processes[1].command, "med_oom");
assert_eq!(table.processes[2].command, "low_oom");
}
#[test]
fn test_process_table_sort_oom_toggle_ascending() {
let mut table = ProcessTable::new();
let entries = vec![
ProcessEntry::new(1, "user", 10.0, 5.0, "low_oom").with_oom_score(100),
ProcessEntry::new(2, "user", 10.0, 5.0, "high_oom").with_oom_score(800),
];
table.set_processes(entries);
table.sort_by(ProcessSort::Oom);
table.sort_by(ProcessSort::Oom);
assert_eq!(table.processes[0].command, "low_oom");
assert_eq!(table.processes[1].command, "high_oom");
}
struct MockCanvas {
texts: Vec<(String, Point)>,
rects: Vec<Rect>,
}
impl MockCanvas {
fn new() -> Self {
Self {
texts: vec![],
rects: vec![],
}
}
}
impl Canvas for MockCanvas {
fn fill_rect(&mut self, rect: Rect, _color: Color) {
self.rects.push(rect);
}
fn stroke_rect(&mut self, _rect: Rect, _color: Color, _width: f32) {}
fn draw_text(&mut self, text: &str, position: Point, _style: &TextStyle) {
self.texts.push((text.to_string(), position));
}
fn draw_line(&mut self, _from: Point, _to: Point, _color: Color, _width: f32) {}
fn fill_circle(&mut self, _center: Point, _radius: f32, _color: Color) {}
fn stroke_circle(&mut self, _center: Point, _radius: f32, _color: Color, _width: f32) {}
fn fill_arc(&mut self, _c: Point, _r: f32, _s: f32, _e: f32, _color: Color) {}
fn draw_path(&mut self, _points: &[Point], _color: Color, _width: f32) {}
fn fill_polygon(&mut self, _points: &[Point], _color: Color) {}
fn push_clip(&mut self, _rect: Rect) {}
fn pop_clip(&mut self) {}
fn push_transform(&mut self, _transform: presentar_core::Transform2D) {}
fn pop_transform(&mut self) {}
}
#[test]
fn test_process_table_paint_basic() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(!canvas.texts.is_empty());
assert!(canvas.texts.iter().any(|(t, _)| t.contains("PID")));
}
#[test]
fn test_process_table_paint_compact() {
let mut table = ProcessTable::new().compact();
table.set_processes(sample_processes());
table.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas.texts.iter().any(|(t, _)| t.contains("C%")));
assert!(canvas.texts.iter().any(|(t, _)| t.contains("M%")));
}
#[test]
fn test_process_table_paint_with_oom() {
let mut table = ProcessTable::new().with_oom();
let entries = vec![
ProcessEntry::new(1, "user", 10.0, 5.0, "low_oom").with_oom_score(100),
ProcessEntry::new(2, "user", 10.0, 5.0, "high_oom").with_oom_score(800),
ProcessEntry::new(3, "user", 10.0, 5.0, "med_oom").with_oom_score(400),
];
table.set_processes(entries);
table.bounds = Rect::new(0.0, 0.0, 100.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas.texts.iter().any(|(t, _)| t.contains("OOM")));
assert!(canvas.texts.iter().any(|(t, _)| t.contains("100")));
assert!(canvas.texts.iter().any(|(t, _)| t.contains("800")));
}
#[test]
fn test_process_table_paint_with_nice() {
let mut table = ProcessTable::new().with_nice_column();
let entries = vec![
ProcessEntry::new(1, "user", 10.0, 5.0, "high_pri").with_nice(-10),
ProcessEntry::new(2, "user", 10.0, 5.0, "low_pri").with_nice(10),
ProcessEntry::new(3, "user", 10.0, 5.0, "normal").with_nice(0),
];
table.set_processes(entries);
table.bounds = Rect::new(0.0, 0.0, 100.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas.texts.iter().any(|(t, _)| t.contains("NI")));
}
#[test]
fn test_process_table_paint_with_selection() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.select(1);
table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(!canvas.rects.is_empty());
}
#[test]
fn test_process_table_paint_empty() {
let mut table = ProcessTable::new();
table.bounds = Rect::new(0.0, 0.0, 80.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas.texts.iter().any(|(t, _)| t.contains("No processes")));
}
#[test]
fn test_process_table_paint_zero_bounds() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.bounds = Rect::new(0.0, 0.0, 0.0, 0.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas.texts.is_empty());
}
#[test]
fn test_process_table_paint_with_cmdline() {
let mut table = ProcessTable::new().with_cmdline();
let entries = vec![
ProcessEntry::new(1, "root", 0.5, 0.1, "bash").with_cmdline("/bin/bash --login -i")
];
table.set_processes(entries);
table.bounds = Rect::new(0.0, 0.0, 100.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas
.texts
.iter()
.any(|(t, _)| t.contains("/bin/bash") || t.contains("--login")));
}
#[test]
fn test_process_table_paint_compact_with_state() {
let mut table = ProcessTable::new().compact();
let entries = vec![
ProcessEntry::new(1, "root", 50.0, 10.0, "running").with_state(ProcessState::Running),
ProcessEntry::new(2, "user", 0.0, 0.5, "sleeping").with_state(ProcessState::Sleeping),
ProcessEntry::new(3, "user", 0.0, 0.1, "zombie").with_state(ProcessState::Zombie),
];
table.set_processes(entries);
table.bounds = Rect::new(0.0, 0.0, 60.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas.texts.iter().any(|(t, _)| t == "R")); assert!(canvas.texts.iter().any(|(t, _)| t == "S")); }
#[test]
fn test_process_state_char() {
assert_eq!(ProcessState::Running.char(), 'R');
assert_eq!(ProcessState::Sleeping.char(), 'S');
assert_eq!(ProcessState::DiskWait.char(), 'D');
assert_eq!(ProcessState::Zombie.char(), 'Z');
assert_eq!(ProcessState::Stopped.char(), 'T');
assert_eq!(ProcessState::Idle.char(), 'I');
}
#[test]
fn test_process_state_color() {
let running = ProcessState::Running.color();
let sleeping = ProcessState::Sleeping.color();
let zombie = ProcessState::Zombie.color();
assert_ne!(running, sleeping);
assert_ne!(running, zombie);
}
#[test]
fn test_process_state_default() {
assert_eq!(ProcessState::default(), ProcessState::Sleeping);
}
#[test]
fn test_process_entry_with_state() {
let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_state(ProcessState::Running);
assert_eq!(proc.state, ProcessState::Running);
}
#[test]
fn test_process_entry_with_cgroup() {
let proc =
ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_cgroup("/user.slice/user-1000");
assert_eq!(proc.cgroup.as_deref(), Some("/user.slice/user-1000"));
}
#[test]
fn test_process_entry_with_nice() {
let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_nice(-5);
assert_eq!(proc.nice, Some(-5));
}
#[test]
fn test_process_entry_with_oom_score() {
let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_oom_score(500);
assert_eq!(proc.oom_score, Some(500));
}
#[test]
fn test_process_entry_with_threads() {
let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test").with_threads(42);
assert_eq!(proc.threads, Some(42));
}
#[test]
fn test_process_table_with_threads_column() {
let table = ProcessTable::new().with_threads_column();
assert!(table.show_threads);
}
#[test]
fn test_process_table_scroll() {
let mut table = ProcessTable::new();
let entries: Vec<ProcessEntry> = (0..50)
.map(|i| ProcessEntry::new(i, "user", i as f32, 0.0, format!("proc{i}")))
.collect();
table.set_processes(entries);
table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0); table.layout(table.bounds);
table.select(45);
assert!(table.scroll_offset > 0);
}
#[test]
fn test_process_table_ensure_visible_up() {
let mut table = ProcessTable::new();
let entries: Vec<ProcessEntry> = (0..20)
.map(|i| ProcessEntry::new(i, "user", 0.0, 0.0, format!("proc{i}")))
.collect();
table.set_processes(entries);
table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
table.scroll_offset = 10;
table.selected = 5;
table.ensure_visible();
assert!(table.scroll_offset <= table.selected);
}
#[test]
fn test_process_table_select_empty() {
let mut table = ProcessTable::new();
table.select(5);
table.select_next();
table.select_prev();
assert_eq!(table.selected(), 0);
}
#[test]
fn test_process_table_selected_process_empty() {
let table = ProcessTable::new();
assert!(table.selected_process().is_none());
}
#[test]
fn test_process_table_budget() {
let table = ProcessTable::new();
let budget = table.budget();
assert!(budget.paint_ms > 0);
}
#[test]
fn test_process_table_assertions() {
let table = ProcessTable::new();
assert!(!table.assertions().is_empty());
}
#[test]
fn test_process_table_set_processes_clamp_selection() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.selected = 2; table.set_processes(vec![ProcessEntry::new(1, "root", 0.0, 0.0, "test")]);
assert_eq!(table.selected(), 0);
}
#[test]
fn test_process_table_event_down() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.event(&Event::KeyDown { key: Key::Down });
assert_eq!(table.selected(), 1);
}
#[test]
fn test_process_table_event_up() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.select(2);
table.event(&Event::KeyDown { key: Key::Up });
assert_eq!(table.selected(), 1);
}
#[test]
fn test_process_table_event_c() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.sort_by(ProcessSort::Pid);
table.event(&Event::KeyDown { key: Key::C });
assert_eq!(table.current_sort(), ProcessSort::Cpu);
}
#[test]
fn test_process_table_event_m() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.event(&Event::KeyDown { key: Key::M });
assert_eq!(table.current_sort(), ProcessSort::Memory);
}
#[test]
fn test_process_table_event_n() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.event(&Event::KeyDown { key: Key::N });
assert_eq!(table.current_sort(), ProcessSort::Command);
}
#[test]
fn test_process_table_event_o() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
table.event(&Event::KeyDown { key: Key::O });
assert_eq!(table.current_sort(), ProcessSort::Oom);
}
#[test]
fn test_process_table_event_other() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
let prev_selected = table.selected();
table.event(&Event::KeyDown { key: Key::A });
assert_eq!(table.selected(), prev_selected);
}
#[test]
fn test_process_table_event_non_keydown() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
let result = table.event(&Event::Resize {
width: 100.0,
height: 50.0,
});
assert!(result.is_none());
}
#[test]
fn test_process_table_with_cpu_gradient() {
let gradient = Gradient::from_hex(&["#0000FF", "#FF0000"]);
let table = ProcessTable::new().with_cpu_gradient(gradient);
assert!(!table.is_empty() || table.is_empty());
}
#[test]
fn test_process_table_with_mem_gradient() {
let gradient = Gradient::from_hex(&["#00FF00", "#FF0000"]);
let table = ProcessTable::new().with_mem_gradient(gradient);
assert!(!table.is_empty() || table.is_empty());
}
#[test]
fn test_process_table_measure_compact() {
let table = ProcessTable::new().compact();
let size = table.measure(Constraints::new(0.0, 100.0, 0.0, 50.0));
assert!(size.width >= 40.0); }
#[test]
fn test_process_table_truncate_exact() {
assert_eq!(ProcessTable::truncate("exact", 5), "exact");
}
#[test]
fn test_process_table_truncate_width_1() {
assert_eq!(ProcessTable::truncate("hello", 1), "h");
}
#[test]
fn test_process_table_paint_all_columns() {
let mut table = ProcessTable::new()
.compact()
.with_oom()
.with_nice_column()
.with_cmdline();
let entries = vec![
ProcessEntry::new(1, "root", 50.0, 10.0, "bash")
.with_state(ProcessState::Running)
.with_oom_score(100)
.with_nice(-5)
.with_cmdline("/bin/bash"),
ProcessEntry::new(2, "user", 30.0, 5.0, "vim")
.with_state(ProcessState::Sleeping)
.with_oom_score(600)
.with_nice(10)
.with_cmdline("/usr/bin/vim"),
];
table.set_processes(entries);
table.bounds = Rect::new(0.0, 0.0, 120.0, 20.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas.texts.iter().any(|(t, _)| t.contains("PID")));
assert!(canvas.texts.iter().any(|(t, _)| t.contains("OOM")));
assert!(canvas.texts.iter().any(|(t, _)| t.contains("NI")));
}
#[test]
fn test_process_entry_clone() {
let proc = ProcessEntry::new(1, "root", 50.0, 10.0, "test")
.with_state(ProcessState::Running)
.with_oom_score(100);
let cloned = proc.clone();
assert_eq!(cloned.pid, proc.pid);
assert_eq!(cloned.state, proc.state);
}
#[test]
fn test_process_entry_debug() {
let proc = ProcessEntry::new(1, "root", 0.0, 0.0, "test");
let debug = format!("{:?}", proc);
assert!(debug.contains("ProcessEntry"));
}
#[test]
fn test_process_sort_debug() {
let sort = ProcessSort::Cpu;
let debug = format!("{:?}", sort);
assert!(debug.contains("Cpu"));
}
#[test]
fn test_process_table_clone() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
let cloned = table.clone();
assert_eq!(cloned.len(), table.len());
}
#[test]
fn test_process_table_debug() {
let table = ProcessTable::new();
let debug = format!("{:?}", table);
assert!(debug.contains("ProcessTable"));
}
#[test]
fn test_process_state_debug() {
let state = ProcessState::Running;
let debug = format!("{:?}", state);
assert!(debug.contains("Running"));
}
#[test]
fn test_process_entry_with_parent_pid() {
let proc = ProcessEntry::new(100, "user", 10.0, 5.0, "child").with_parent_pid(1);
assert_eq!(proc.parent_pid, Some(1));
}
#[test]
fn test_process_entry_set_tree_info() {
let mut proc = ProcessEntry::new(100, "user", 10.0, 5.0, "child");
proc.set_tree_info(2, true, "│ └─".to_string());
assert_eq!(proc.tree_depth, 2);
assert!(proc.is_last_child);
assert_eq!(proc.tree_prefix, "│ └─");
}
#[test]
fn test_process_table_with_tree_view() {
let table = ProcessTable::new().with_tree_view();
assert!(table.is_tree_view());
}
#[test]
fn test_process_table_toggle_tree_view() {
let mut table = ProcessTable::new();
assert!(!table.is_tree_view());
table.toggle_tree_view();
assert!(table.is_tree_view());
table.toggle_tree_view();
assert!(!table.is_tree_view());
}
#[test]
fn test_process_table_tree_view_builds_tree() {
let mut table = ProcessTable::new().with_tree_view();
let entries = vec![
ProcessEntry::new(200, "user", 5.0, 1.0, "vim").with_parent_pid(100),
ProcessEntry::new(100, "user", 10.0, 2.0, "bash").with_parent_pid(1),
ProcessEntry::new(101, "root", 1.0, 0.5, "sshd").with_parent_pid(1),
ProcessEntry::new(1, "root", 0.5, 0.1, "systemd"),
];
table.set_processes(entries);
assert_eq!(table.processes[0].command, "systemd");
assert_eq!(table.processes[0].tree_prefix, "");
}
#[test]
fn test_process_table_tree_view_prefix_chars() {
let mut table = ProcessTable::new().with_tree_view();
let entries = vec![
ProcessEntry::new(3, "user", 5.0, 1.0, "grandchild").with_parent_pid(2),
ProcessEntry::new(2, "user", 10.0, 2.0, "child").with_parent_pid(1),
ProcessEntry::new(1, "root", 0.5, 0.1, "parent"),
];
table.set_processes(entries);
assert_eq!(table.processes[0].tree_prefix, "");
assert!(
table.processes[1].tree_prefix.contains('└')
|| table.processes[1].tree_prefix.contains('├')
);
}
#[test]
fn test_process_table_event_t_toggles_tree() {
let mut table = ProcessTable::new();
table.set_processes(sample_processes());
assert!(!table.is_tree_view());
table.event(&Event::KeyDown { key: Key::T });
assert!(table.is_tree_view());
table.event(&Event::KeyDown { key: Key::T });
assert!(!table.is_tree_view());
}
#[test]
fn test_process_table_tree_view_paint() {
let mut table = ProcessTable::new().with_tree_view();
let entries = vec![
ProcessEntry::new(2, "user", 10.0, 2.0, "child").with_parent_pid(1),
ProcessEntry::new(1, "root", 0.5, 0.1, "parent"),
];
table.set_processes(entries);
table.bounds = Rect::new(0.0, 0.0, 80.0, 10.0);
let mut canvas = MockCanvas::new();
table.paint(&mut canvas);
assert!(canvas
.texts
.iter()
.any(|(t, _)| t.contains("└") || t.contains("├")));
}
#[test]
fn test_process_table_tree_empty() {
let mut table = ProcessTable::new().with_tree_view();
table.set_processes(vec![]);
assert!(table.is_empty());
}
#[test]
fn test_f_tree_001_hierarchy_overrides_sorting() {
let mut table = ProcessTable::new().with_tree_view();
let entries = vec![
ProcessEntry::new(999, "root", 99.0, 50.0, "chrome"), ProcessEntry::new(200, "user", 0.1, 0.1, "sleep").with_parent_pid(100),
ProcessEntry::new(201, "user", 0.1, 0.1, "sleep").with_parent_pid(100),
ProcessEntry::new(100, "user", 1.0, 0.5, "sh"),
ProcessEntry::new(1, "root", 0.5, 0.1, "systemd"),
];
table.set_processes(entries);
let sh_idx = table
.processes
.iter()
.position(|p| p.command == "sh")
.expect("sh not found");
let sleep1_idx = table
.processes
.iter()
.position(|p| p.command == "sleep" && p.pid == 200)
.expect("sleep 200 not found");
let sleep2_idx = table
.processes
.iter()
.position(|p| p.command == "sleep" && p.pid == 201)
.expect("sleep 201 not found");
assert!(
sleep1_idx > sh_idx && sleep1_idx <= sh_idx + 2,
"sleep 200 (idx {}) should be immediately after sh (idx {})",
sleep1_idx,
sh_idx
);
assert!(
sleep2_idx > sh_idx && sleep2_idx <= sh_idx + 2,
"sleep 201 (idx {}) should be immediately after sh (idx {})",
sleep2_idx,
sh_idx
);
let chrome_idx = table
.processes
.iter()
.position(|p| p.command == "chrome")
.expect("chrome not found");
assert!(
!(chrome_idx > sh_idx && chrome_idx < sleep1_idx.max(sleep2_idx)),
"Unrelated process should not split parent-child hierarchy"
);
}
#[test]
fn test_f_tree_002_orphan_handling() {
let mut table = ProcessTable::new().with_tree_view();
let entries = vec![
ProcessEntry::new(200, "user", 5.0, 1.0, "orphan1").with_parent_pid(100), ProcessEntry::new(201, "user", 3.0, 1.0, "orphan2").with_parent_pid(100), ProcessEntry::new(1, "root", 0.5, 0.1, "systemd"),
];
table.set_processes(entries);
assert_eq!(table.len(), 3);
let orphan1 = table
.processes
.iter()
.find(|p| p.command == "orphan1")
.unwrap();
let orphan2 = table
.processes
.iter()
.find(|p| p.command == "orphan2")
.unwrap();
assert_eq!(orphan1.tree_depth, 0);
assert_eq!(orphan2.tree_depth, 0);
}
#[test]
fn test_f_tree_003_deep_nesting_15_levels() {
let mut table = ProcessTable::new().with_tree_view();
let mut entries = vec![ProcessEntry::new(1, "root", 0.5, 0.1, "init")];
for depth in 1..=15 {
let pid = (depth + 1) as u32;
let ppid = depth as u32;
entries.push(
ProcessEntry::new(pid, "user", 0.1, 0.1, format!("level{depth}"))
.with_parent_pid(ppid),
);
}
table.set_processes(entries);
assert_eq!(table.len(), 16);
let deepest = table
.processes
.iter()
.find(|p| p.command == "level15")
.unwrap();
assert_eq!(deepest.tree_depth, 15);
let prefix_segments =
deepest.tree_prefix.matches("│").count() + deepest.tree_prefix.matches(" ").count();
assert!(
deepest.tree_prefix.len() > 20,
"Deep prefix should be substantial: '{}'",
deepest.tree_prefix
);
}
#[test]
fn test_f_tree_004_dfs_traversal_order() {
let mut table = ProcessTable::new().with_tree_view();
let entries = vec![
ProcessEntry::new(5, "user", 1.0, 1.0, "E").with_parent_pid(2),
ProcessEntry::new(4, "user", 1.0, 1.0, "D").with_parent_pid(2),
ProcessEntry::new(3, "user", 1.0, 1.0, "C").with_parent_pid(1),
ProcessEntry::new(2, "user", 2.0, 1.0, "B").with_parent_pid(1), ProcessEntry::new(1, "root", 0.5, 0.1, "A"),
];
table.set_processes(entries);
let commands: Vec<&str> = table.processes.iter().map(|p| p.command.as_str()).collect();
assert_eq!(commands[0], "A", "Root should be first");
assert_eq!(commands[1], "B", "B (higher CPU child) should be second");
assert!(
commands[2] == "D" || commands[2] == "E",
"B's children should follow B"
);
assert!(
commands[3] == "D" || commands[3] == "E",
"B's children should follow B"
);
assert_eq!(commands[4], "C", "C should be after B's subtree");
}
}