Skip to main content

proc_cli/commands/
tree.rs

1//! Tree command - Show process tree
2//!
3//! Usage:
4//!   proc tree              # Full process tree
5//!   proc tree node         # Tree for the processes named node
6//!   proc tree node --cmd   # ... or with node anywhere in their command line
7//!   proc tree :3000        # Tree for process on port 3000
8//!   proc tree 1234         # Tree for PID 1234
9//!   proc tree --min-cpu 10 # Only processes using >10% CPU
10//!   proc tree 1234 -a      # Show ancestry (path UP to root)
11
12use crate::core::{
13    classify, matches_by_filter, matches_in_filter, parse_target, resolve_in_dir, resolve_target,
14    ruled_out_by, Guards, Hit, NameMatch, Process, ProcessStatus, StatusFilter, TargetType,
15};
16use crate::error::{ProcError, Result};
17use crate::out_println;
18use crate::ui::{format_memory, plural, uptime_label, Printer};
19use clap::Args;
20use colored::*;
21use serde::Serialize;
22use std::collections::HashMap;
23
24/// Show process tree
25#[derive(Args, Debug)]
26pub struct TreeCommand {
27    /// Target: process name, :port, or PID (shows full tree if omitted)
28    #[arg(value_parser = crate::core::validate_target_arg)]
29    target: Option<String>,
30
31    /// Show ancestry (path UP to root) instead of descendants
32    #[arg(long, short)]
33    ancestors: bool,
34
35    /// Show verbose output
36    #[arg(long, short = 'v')]
37    verbose: bool,
38
39    /// Output as JSON
40    #[arg(long, short = 'j')]
41    json: bool,
42
43    /// Maximum depth to display
44    #[arg(long, short, default_value = "10")]
45    depth: usize,
46
47    /// Show PIDs only (compact view)
48    #[arg(long, short = 'C')]
49    compact: bool,
50
51    /// Only show processes using more than this CPU %
52    #[arg(long)]
53    min_cpu: Option<f32>,
54
55    /// Only show processes using more than this memory (MB)
56    #[arg(long)]
57    min_mem: Option<f64>,
58
59    /// Filter by status: running, sleeping, stopped, zombie
60    #[arg(long, value_enum, ignore_case = true)]
61    status: Option<StatusFilter>,
62
63    /// Only show processes running longer than this (seconds)
64    #[arg(long)]
65    min_uptime: Option<u64>,
66
67    /// Filter by directory; takes the next word as the directory unless it starts with '-'
68    /// (bare --in is the current directory, --in=DIR is explicit)
69    #[arg(long = "in", short = 'i', num_args = 0..=1, default_missing_value = ".")]
70    pub in_dir: Option<String>,
71
72    /// Filter by process name
73    #[arg(long = "by", short = 'b')]
74    pub by_name: Option<String>,
75
76    /// Match name targets and --by as a substring of the process name or full command line (like pkill -f)
77    #[arg(long)]
78    pub cmd: bool,
79}
80
81impl TreeCommand {
82    /// The matches to draw trees from. A match that a root above it draws in
83    /// its tree (no more than `--depth` levels down) is not a root itself:
84    /// drawing it again showed it twice (a shell and what it starts share a
85    /// cwd, so `--in` matches both). Roots are chosen top down, so a match
86    /// counts as drawn only under one that is drawn. The walk up is bounded,
87    /// so a loop in the parent links cannot hold it.
88    fn topmost<'a>(
89        &self,
90        matches: Vec<&'a Process>,
91        pid_map: &HashMap<u32, &Process>,
92    ) -> Vec<&'a Process> {
93        // each match with the PIDs above it, nearest first
94        let above: Vec<(&Process, Vec<u32>)> = matches
95            .into_iter()
96            .map(|p| {
97                let up = std::iter::successors(p.parent_pid, |ppid| {
98                    pid_map.get(ppid).and_then(|q| q.parent_pid)
99                })
100                .take(pid_map.len())
101                .collect();
102                (p, up)
103            })
104            .collect();
105        let mut top_down: Vec<&(&Process, Vec<u32>)> = above.iter().collect();
106        top_down.sort_by_key(|(_, up)| up.len());
107        let mut roots = std::collections::HashSet::new();
108        for (p, up) in top_down {
109            if !up.iter().take(self.depth).any(|pid| roots.contains(pid)) {
110                roots.insert(p.pid);
111            }
112        }
113        above
114            .into_iter()
115            .filter(|(p, _)| roots.contains(&p.pid))
116            .map(|(p, _)| p)
117            .collect()
118    }
119
120    /// Build the set of ancestor PIDs to exclude from name matching.
121    ///
122    /// Walks up the process tree from the current process to avoid matching
123    /// shells whose command lines contain proc's own arguments. `all_processes`
124    /// leaves proc itself out, so its parent is looked up separately.
125    fn ancestor_pids(all_processes: &[Process]) -> std::collections::HashSet<u32> {
126        let mut pids = std::collections::HashSet::new();
127        let self_pid = std::process::id();
128        pids.insert(self_pid);
129        let mut next = Process::find_by_pid(self_pid)
130            .ok()
131            .flatten()
132            .and_then(|me| me.parent_pid);
133        for _ in 0..10 {
134            let Some(parent_pid) = next else { break };
135            pids.insert(parent_pid);
136            next = all_processes
137                .iter()
138                .find(|p| p.pid == parent_pid)
139                .and_then(|p| p.parent_pid);
140        }
141        pids
142    }
143
144    /// The processes a name matches, leaving out `ancestors` (proc and the shells
145    /// above it), and how many others have the name only in their command line
146    /// (what `--cmd` would add): both come from the one pass over `all`.
147    fn named<'a>(
148        all: impl Iterator<Item = &'a Process>,
149        ancestors: &std::collections::HashSet<u32>,
150        pattern: &str,
151        mode: NameMatch,
152    ) -> (Vec<&'a Process>, usize) {
153        let mut others = 0;
154        let named = all
155            .filter(|p| !ancestors.contains(&p.pid))
156            .filter(|p| match classify(pattern, &p.names(), mode) {
157                Hit::Match => true,
158                Hit::CommandLineOnly => {
159                    others += 1;
160                    false
161                }
162                Hit::No => false,
163            })
164            .collect();
165        (named, others)
166    }
167
168    /// Executes the tree command, displaying the process hierarchy.
169    pub fn execute(&self) -> Result<()> {
170        let printer = Printer::from_flags(self.json, self.verbose);
171
172        // Ancestry is of a target; with none there is nothing to trace, and
173        // answering with an empty result would look like success
174        let ancestry_of = match (self.ancestors, &self.target) {
175            (true, None) => {
176                return Err(ProcError::InvalidInput(
177                    "--ancestors needs a target (PID, :port, or name)".to_string(),
178                ))
179            }
180            (true, Some(target)) => Some(target),
181            (false, _) => None,
182        };
183
184        // Get all processes, with real CPU readings for the nodes and --min-cpu
185        let mut all_processes = Process::find_all()?;
186        Process::measure_cpu(&mut all_processes);
187
188        // The one place the tree's order is decided: PID ascending. The roots,
189        // every parent's children, the filter list, the target list and the
190        // ancestry targets all come from walking this list (or `children_map`,
191        // built from it), so the human tree and `--json` read alike.
192        all_processes.sort_by_key(|p| p.pid);
193
194        // Build PID -> Process map for quick lookup
195        let pid_map: HashMap<u32, &Process> = all_processes.iter().map(|p| (p.pid, p)).collect();
196
197        // Build parent -> children map
198        let mut children_map: HashMap<u32, Vec<&Process>> = HashMap::new();
199
200        for proc in &all_processes {
201            if let Some(ppid) = proc.parent_pid {
202                children_map.entry(ppid).or_default().push(proc);
203            }
204        }
205
206        // --in and --by narrow a target's processes. With no target they pick
207        // the processes shown, each with its subtree, as --min-cpu does; they
208        // used to be ignored there, so `tree --in .` printed the whole system
209        let mode = NameMatch::from_flag(self.cmd);
210        let in_dir_filter = resolve_in_dir(&self.in_dir)?;
211        let in_dir_has_any = in_dir_filter
212            .as_ref()
213            .is_some_and(|dir_path| all_processes.iter().any(|p| matches_in_filter(p, dir_path)));
214        crate::core::require_in_dir(&self.in_dir, &in_dir_filter, in_dir_has_any)?;
215        let in_and_by = |p: &Process| {
216            in_dir_filter
217                .as_ref()
218                .is_none_or(|dir_path| matches_in_filter(p, dir_path))
219                && self
220                    .by_name
221                    .as_ref()
222                    .is_none_or(|name| matches_by_filter(p, name, mode))
223        };
224
225        // Handle --ancestors mode
226        if let Some(target) = ancestry_of {
227            return self.show_ancestors(&printer, target, &pid_map, &all_processes, &in_and_by);
228        }
229
230        // Determine target processes
231        let target_processes: Vec<&Process> = if let Some(ref target) = self.target {
232            // Use unified target resolution
233            match parse_target(target) {
234                TargetType::Port(_) | TargetType::Pid(_) => {
235                    // For port or PID, resolve to specific process(es)
236                    // Errors (not found) rather than returning nothing
237                    let resolved = resolve_target(target, mode)?;
238                    // Find matching processes in all_processes
239                    let pids: Vec<u32> = resolved.iter().map(|p| p.pid).collect();
240                    all_processes
241                        .iter()
242                        .filter(|p| pids.contains(&p.pid))
243                        .collect()
244                }
245                TargetType::Name(ref pattern) => {
246                    // For name, do pattern matching (exclude self and parent to avoid
247                    // false positives from proc's own args in the shell command line)
248                    let ancestor_pids = Self::ancestor_pids(&all_processes);
249                    let (named, others) =
250                        Self::named(all_processes.iter(), &ancestor_pids, pattern, mode);
251                    // A name that matches nothing is not found, as a PID or a
252                    // port that matches nothing is
253                    if named.is_empty() {
254                        return Err(ProcError::name_not_found(pattern, others));
255                    }
256                    named
257                }
258            }
259        } else {
260            Vec::new() // Will show full tree
261        };
262
263        let target_processes: Vec<&Process> = if let Some(target) = &self.target {
264            let found: Vec<Process> = target_processes.iter().map(|p| (*p).clone()).collect();
265            let kept: Vec<&Process> = target_processes
266                .into_iter()
267                .filter(|p| in_and_by(p))
268                .collect();
269            // The target found processes (none would have been an error above),
270            // so an empty list means --in or --by ruled them all out: the same
271            // guard error as `info` and `on`, not an empty answer
272            if kept.is_empty() {
273                let guards = Guards::new(&self.in_dir, &self.by_name, None);
274                return Err(ProcError::FilteredOut {
275                    target: target.clone(),
276                    ruled_out_by: ruled_out_by(&found, &guards, mode)?,
277                });
278            }
279            kept
280        } else {
281            target_processes
282        };
283
284        // Apply resource filters if specified
285        let matches_filters = |p: &Process| self.matches_filters(p);
286
287        // Apply filters to target processes or find filtered roots
288        let has_filters = in_dir_filter.is_some()
289            || self.by_name.is_some()
290            || self.min_cpu.is_some()
291            || self.min_mem.is_some()
292            || self.status.is_some()
293            || self.min_uptime.is_some();
294
295        if self.json {
296            let tree_nodes = if self.target.is_some() {
297                target_processes
298                    .iter()
299                    .filter(|p| matches_filters(p))
300                    .map(|p| self.build_tree_node(p, &children_map, 0))
301                    .collect()
302            } else if has_filters {
303                // Show only processes matching filters, each once
304                let matches = all_processes
305                    .iter()
306                    .filter(|p| in_and_by(p) && matches_filters(p))
307                    .collect();
308                self.topmost(matches, &pid_map)
309                    .into_iter()
310                    .map(|p| self.build_tree_node(p, &children_map, 0))
311                    .collect()
312            } else {
313                // Show full tree from roots
314                all_processes
315                    .iter()
316                    .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
317                    .map(|p| self.build_tree_node(p, &children_map, 0))
318                    .collect()
319            };
320
321            printer.print_json(&TreeOutput {
322                action: "tree",
323                success: true,
324                tree: tree_nodes,
325            });
326        } else if self.target.is_some() {
327            let filtered: Vec<_> = target_processes
328                .into_iter()
329                .filter(|p| matches_filters(p))
330                .collect();
331            if filtered.is_empty() {
332                // Only --min-cpu, --min-mem, --status or --min-uptime get here:
333                // they narrow the view, so nothing left is an empty answer, as
334                // in `list`
335                let target = self.target.as_ref().unwrap();
336                printer.warning(&format!(
337                    "No processes for '{target}' match the specified filters"
338                ));
339                return Ok(());
340            }
341
342            out_println!(
343                "{} Process tree for '{}':\n",
344                "✓".green().bold(),
345                self.target.as_ref().unwrap().cyan()
346            );
347
348            for proc in &filtered {
349                self.print_tree(proc, &children_map, "", true, 0);
350                out_println!();
351            }
352        } else if has_filters {
353            let filtered: Vec<_> = all_processes
354                .iter()
355                .filter(|p| in_and_by(p) && matches_filters(p))
356                .collect();
357            if filtered.is_empty() {
358                printer.warning("No processes match the specified filters");
359                return Ok(());
360            }
361
362            out_println!(
363                "{} {} process{} matching filters:\n",
364                "✓".green().bold(),
365                filtered.len().to_string().cyan().bold(),
366                plural(filtered.len())
367            );
368
369            let roots = self.topmost(filtered, &pid_map);
370            for (i, proc) in roots.iter().enumerate() {
371                let is_last = i == roots.len() - 1;
372                self.print_tree(proc, &children_map, "", is_last, 0);
373            }
374        } else {
375            out_println!("{} Process tree:\n", "✓".green().bold());
376
377            // Find processes with PID 1 or no parent as roots
378            let display_roots: Vec<&Process> = all_processes
379                .iter()
380                .filter(|p| p.parent_pid.is_none() || p.parent_pid == Some(0))
381                .collect();
382
383            for (i, proc) in display_roots.iter().enumerate() {
384                let is_last = i == display_roots.len() - 1;
385                self.print_tree(proc, &children_map, "", is_last, 0);
386            }
387        }
388
389        Ok(())
390    }
391
392    /// Whether a process passes `--min-cpu`, `--min-mem`, `--status` and `--min-uptime`
393    fn matches_filters(&self, p: &Process) -> bool {
394        if let Some(min_cpu) = self.min_cpu {
395            if p.cpu_percent < min_cpu {
396                return false;
397            }
398        }
399        if let Some(min_mem) = self.min_mem {
400            if p.memory_mb < min_mem {
401                return false;
402            }
403        }
404        if let Some(status) = self.status {
405            if !status.matches(p.status) {
406                return false;
407            }
408        }
409        if let Some(min_uptime) = self.min_uptime {
410            if let Some(start_time) = p.start_time {
411                let now = std::time::SystemTime::now()
412                    .duration_since(std::time::UNIX_EPOCH)
413                    .map(|d| d.as_secs())
414                    .unwrap_or(0);
415                if now.saturating_sub(start_time) < min_uptime {
416                    return false;
417                }
418            } else {
419                return false;
420            }
421        }
422        true
423    }
424
425    fn print_tree(
426        &self,
427        proc: &Process,
428        children_map: &HashMap<u32, Vec<&Process>>,
429        prefix: &str,
430        is_last: bool,
431        depth: usize,
432    ) {
433        if depth > self.depth {
434            return;
435        }
436
437        let connector = if is_last { "└── " } else { "├── " };
438
439        if self.compact {
440            out_println!(
441                "{}{}{}",
442                prefix.bright_black(),
443                connector.bright_black(),
444                proc.pid.to_string().cyan()
445            );
446        } else {
447            let status_indicator = status_indicator(proc.status);
448
449            out_println!(
450                "{}{}{} {} [{}] {:.1}% {}{}",
451                prefix.bright_black(),
452                connector.bright_black(),
453                status_indicator,
454                proc.name.white().bold(),
455                proc.pid.to_string().cyan(),
456                proc.cpu_percent,
457                format_memory(proc.memory_mb),
458                uptime_label(proc.uptime_secs).bright_black()
459            );
460        }
461
462        let child_prefix = if is_last {
463            format!("{}    ", prefix)
464        } else {
465            format!("{}│   ", prefix)
466        };
467
468        // the children are already in PID order (see `execute`)
469        if let Some(children) = children_map.get(&proc.pid) {
470            for (i, child) in children.iter().enumerate() {
471                let child_is_last = i == children.len() - 1;
472                self.print_tree(child, children_map, &child_prefix, child_is_last, depth + 1);
473            }
474        }
475    }
476
477    fn build_tree_node(
478        &self,
479        proc: &Process,
480        children_map: &HashMap<u32, Vec<&Process>>,
481        depth: usize,
482    ) -> TreeNode {
483        let children = if depth < self.depth {
484            children_map
485                .get(&proc.pid)
486                .map(|kids| {
487                    kids.iter()
488                        .map(|p| self.build_tree_node(p, children_map, depth + 1))
489                        .collect()
490                })
491                .unwrap_or_default()
492        } else {
493            Vec::new()
494        };
495
496        TreeNode {
497            pid: proc.pid,
498            name: proc.name.clone(),
499            cpu_percent: proc.cpu_percent,
500            memory_mb: proc.memory_mb,
501            status: proc.status,
502            uptime_secs: proc.uptime_secs,
503            children,
504        }
505    }
506
507    /// Show ancestry (path UP to root) for target processes; `all_processes` is
508    /// the process list in PID order, which the targets of a name follow
509    fn show_ancestors(
510        &self,
511        printer: &Printer,
512        target: &str,
513        pid_map: &HashMap<u32, &Process>,
514        all_processes: &[Process],
515        in_and_by: &dyn Fn(&Process) -> bool,
516    ) -> Result<()> {
517        use crate::core::{parse_target, resolve_target, TargetType};
518
519        let mode = NameMatch::from_flag(self.cmd);
520
521        // Resolve target to processes
522        let target_processes: Vec<Process> = match parse_target(target) {
523            TargetType::Port(_) | TargetType::Pid(_) => resolve_target(target, mode)?,
524            TargetType::Name(ref pattern) => {
525                let ancestor_pids = Self::ancestor_pids(all_processes);
526                let (named, others) =
527                    Self::named(all_processes.iter(), &ancestor_pids, pattern, mode);
528                if named.is_empty() {
529                    return Err(ProcError::name_not_found(pattern, others));
530                }
531                named.into_iter().cloned().collect()
532            }
533        };
534        // --in and --by guard the target here too, as without --ancestors
535        let found = target_processes.clone();
536        let target_processes: Vec<Process> = target_processes
537            .into_iter()
538            .filter(|p| in_and_by(p))
539            .collect();
540        if target_processes.is_empty() {
541            let guards = Guards::new(&self.in_dir, &self.by_name, None);
542            return Err(ProcError::FilteredOut {
543                target: target.to_string(),
544                ruled_out_by: ruled_out_by(&found, &guards, mode)?,
545            });
546        }
547
548        if self.json {
549            let ancestry_output: Vec<AncestryNode> = target_processes
550                .iter()
551                .map(|proc| self.build_ancestry_node(proc, pid_map))
552                .collect();
553            printer.print_json(&AncestryOutput {
554                action: "tree",
555                success: true,
556                ancestry: ancestry_output,
557            });
558        } else {
559            out_println!("{} Ancestry for '{}':\n", "✓".green().bold(), target.cyan());
560
561            for proc in &target_processes {
562                self.print_ancestry(proc, pid_map);
563                out_println!();
564            }
565        }
566
567        Ok(())
568    }
569
570    /// Trace and print ancestry from root down to target
571    fn print_ancestry(&self, target: &Process, pid_map: &HashMap<u32, &Process>) {
572        // Root first, target last
573        let chain = ancestor_chain(target, pid_map);
574
575        // Print the chain
576        for (i, proc) in chain.iter().enumerate() {
577            let is_target = proc.pid == target.pid;
578            let indent = "    ".repeat(i);
579            let connector = if i == 0 { "" } else { "└── " };
580
581            let status_indicator = status_indicator(proc.status);
582
583            if is_target {
584                // Highlight the target
585                out_println!(
586                    "{}{}{} {} [{}] {:.1}% {}{}  {}",
587                    indent.bright_black(),
588                    connector.bright_black(),
589                    status_indicator,
590                    proc.name.cyan().bold(),
591                    proc.pid.to_string().cyan().bold(),
592                    proc.cpu_percent,
593                    format_memory(proc.memory_mb),
594                    uptime_label(proc.uptime_secs).bright_black(),
595                    "← target".yellow()
596                );
597            } else {
598                out_println!(
599                    "{}{}{} {} [{}] {:.1}% {}{}",
600                    indent.bright_black(),
601                    connector.bright_black(),
602                    status_indicator,
603                    proc.name.white(),
604                    proc.pid.to_string().cyan(),
605                    proc.cpu_percent,
606                    format_memory(proc.memory_mb),
607                    uptime_label(proc.uptime_secs).bright_black()
608                );
609            }
610        }
611    }
612
613    /// Build ancestry node for JSON output
614    fn build_ancestry_node(
615        &self,
616        target: &Process,
617        pid_map: &HashMap<u32, &Process>,
618    ) -> AncestryNode {
619        let chain: Vec<ProcessInfo> = ancestor_chain(target, pid_map)
620            .into_iter()
621            .map(|proc| ProcessInfo {
622                pid: proc.pid,
623                name: proc.name.clone(),
624                cpu_percent: proc.cpu_percent,
625                memory_mb: proc.memory_mb,
626                status: proc.status,
627                uptime_secs: proc.uptime_secs,
628            })
629            .collect();
630
631        AncestryNode {
632            target_pid: target.pid,
633            target_name: target.name.clone(),
634            depth: chain.len(),
635            chain,
636        }
637    }
638}
639
640/// The marker for a process status in the tree
641fn status_indicator(status: ProcessStatus) -> ColoredString {
642    match status {
643        ProcessStatus::Running => "●".green(),
644        ProcessStatus::Sleeping => "○".blue(),
645        ProcessStatus::Stopped => "◐".yellow(),
646        ProcessStatus::Zombie => "✗".red(),
647        _ => "?".white(),
648    }
649}
650
651/// The processes from the root down to `target`, found by following parent
652/// PIDs. The chain ends at a parent the process list doesn't hold (the root's
653/// parent is 0, or the parent is gone) and is cut at 100 so a loop in the
654/// parent links cannot run forever.
655fn ancestor_chain<'a>(
656    target: &'a Process,
657    pid_map: &HashMap<u32, &'a Process>,
658) -> Vec<&'a Process> {
659    let mut chain: Vec<&Process> = Vec::new();
660    let mut current_pid = Some(target.pid);
661
662    while let Some(pid) = current_pid {
663        let Some(proc) = pid_map.get(&pid) else { break };
664        chain.push(proc);
665        current_pid = proc.parent_pid;
666        if chain.len() > 100 {
667            break;
668        }
669    }
670
671    chain.reverse();
672    chain
673}
674
675#[derive(Serialize)]
676struct AncestryOutput {
677    action: &'static str,
678    success: bool,
679    ancestry: Vec<AncestryNode>,
680}
681
682#[derive(Serialize)]
683struct AncestryNode {
684    target_pid: u32,
685    target_name: String,
686    depth: usize,
687    chain: Vec<ProcessInfo>,
688}
689
690#[derive(Serialize)]
691struct ProcessInfo {
692    pid: u32,
693    name: String,
694    cpu_percent: f32,
695    memory_mb: f64,
696    status: ProcessStatus,
697    #[serde(skip_serializing_if = "Option::is_none")]
698    uptime_secs: Option<u64>,
699}
700
701#[derive(Serialize)]
702struct TreeOutput {
703    action: &'static str,
704    success: bool,
705    tree: Vec<TreeNode>,
706}
707
708#[derive(Serialize)]
709struct TreeNode {
710    pid: u32,
711    name: String,
712    cpu_percent: f32,
713    memory_mb: f64,
714    status: ProcessStatus,
715    #[serde(skip_serializing_if = "Option::is_none")]
716    uptime_secs: Option<u64>,
717    children: Vec<TreeNode>,
718}
719
720#[cfg(test)]
721mod tests {
722    use super::*;
723
724    fn command() -> TreeCommand {
725        TreeCommand {
726            target: None,
727            ancestors: false,
728            verbose: false,
729            json: false,
730            depth: 10,
731            compact: false,
732            min_cpu: None,
733            min_mem: None,
734            status: None,
735            min_uptime: None,
736            in_dir: None,
737            by_name: None,
738            cmd: false,
739        }
740    }
741
742    /// A process record with no start time, which some processes really are
743    fn process(pid: u32, parent_pid: Option<u32>) -> Process {
744        Process {
745            pid,
746            name: format!("p{pid}"),
747            exe_path: None,
748            cwd: None,
749            command: None,
750            argv0: None,
751            raw: None,
752            cpu_percent: 0.0,
753            memory_mb: 0.0,
754            status: ProcessStatus::Sleeping,
755            user: None,
756            parent_pid,
757            start_time: None,
758            uptime_secs: None,
759        }
760    }
761
762    #[test]
763    fn a_name_matches_whole_and_counts_the_processes_only_cmd_would_add() {
764        let mut node = process(1, None);
765        node.name = "node".to_string();
766        let mut editor = process(2, None);
767        editor.name = "vim".to_string();
768        editor.command = Some("vim node-notes.md".to_string());
769        let mut shell = process(3, None);
770        shell.name = "node".to_string();
771        let all = [node, editor, shell];
772        let skipped: std::collections::HashSet<u32> = [3].into();
773
774        let (named, others) = TreeCommand::named(all.iter(), &skipped, "node", NameMatch::Exact);
775        assert_eq!(named.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![1]);
776        assert_eq!(others, 1);
777
778        let (named, others) = TreeCommand::named(all.iter(), &skipped, "node", NameMatch::Command);
779        assert_eq!(named.iter().map(|p| p.pid).collect::<Vec<_>>(), vec![1, 2]);
780        assert_eq!(others, 0);
781
782        let (named, others) = TreeCommand::named(all.iter(), &skipped, "python", NameMatch::Exact);
783        assert!(named.is_empty());
784        assert_eq!(others, 0);
785    }
786
787    #[test]
788    fn a_process_with_no_start_time_fails_min_uptime() {
789        let cmd = TreeCommand {
790            min_uptime: Some(0),
791            ..command()
792        };
793        assert!(!cmd.matches_filters(&process(1, None)));
794        assert!(command().matches_filters(&process(1, None)));
795    }
796
797    #[test]
798    fn every_status_has_its_own_marker() {
799        let color = |status| status_indicator(status).fgcolor;
800        let text = |status| status_indicator(status).trim().to_string();
801        assert_eq!(text(ProcessStatus::Running), "●");
802        assert_eq!(text(ProcessStatus::Sleeping), "○");
803        assert_eq!(text(ProcessStatus::Stopped), "◐");
804        assert_eq!(text(ProcessStatus::Zombie), "✗");
805        assert_eq!(text(ProcessStatus::Dead), "?");
806        assert_eq!(text(ProcessStatus::Unknown), "?");
807        assert_eq!(color(ProcessStatus::Running), Some(Color::Green));
808        assert_eq!(color(ProcessStatus::Sleeping), Some(Color::Blue));
809        assert_eq!(color(ProcessStatus::Stopped), Some(Color::Yellow));
810        assert_eq!(color(ProcessStatus::Zombie), Some(Color::Red));
811        assert_eq!(color(ProcessStatus::Unknown), Some(Color::White));
812    }
813
814    #[test]
815    fn the_chain_runs_from_the_root_down_to_the_target() {
816        let (root, middle, leaf) = (
817            process(1, Some(0)),
818            process(2, Some(1)),
819            process(3, Some(2)),
820        );
821        let map: HashMap<u32, &Process> = [&root, &middle, &leaf]
822            .iter()
823            .map(|p| (p.pid, *p))
824            .collect();
825        let chain: Vec<u32> = ancestor_chain(&leaf, &map).iter().map(|p| p.pid).collect();
826        assert_eq!(chain, vec![1, 2, 3]);
827    }
828
829    #[test]
830    fn a_loop_in_the_parent_links_is_cut() {
831        let (a, b) = (process(1, Some(2)), process(2, Some(1)));
832        let map: HashMap<u32, &Process> = [&a, &b].iter().map(|p| (p.pid, *p)).collect();
833        assert_eq!(ancestor_chain(&a, &map).len(), 101);
834    }
835
836    #[test]
837    fn a_process_that_passes_every_filter_is_kept() {
838        let mut p = process(1, None);
839        p.start_time = Some(0);
840        let cmd = TreeCommand {
841            min_cpu: Some(0.0),
842            min_mem: Some(0.0),
843            status: Some(StatusFilter::Sleeping),
844            min_uptime: Some(1),
845            ..command()
846        };
847        assert!(cmd.matches_filters(&p));
848        let other_status = TreeCommand {
849            status: Some(StatusFilter::Running),
850            ..command()
851        };
852        assert!(!other_status.matches_filters(&p));
853    }
854}