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