mod common;
use std::fs;
use std::process::Command;
use std::process::Stdio;
fn leading_spaces(line: &str) -> usize {
line.chars().take_while(|c| *c == ' ').count()
}
fn assert_contains(output: &str, needle: &str, context: &str) {
assert!(
output.contains(needle),
"{context}: expected to find {needle:?} in output:\n{output}"
);
}
fn current_username() -> Option<String> {
let output = Command::new("id")
.arg("-un")
.stdin(Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
let username = String::from_utf8(output.stdout).ok()?;
let username = username.trim().to_string();
if username.is_empty() {
None
} else {
Some(username)
}
}
fn output_has_pid_line(output: &str, pid: u64) -> bool {
let prefix = format!("{pid} ");
output
.lines()
.any(|line| line.trim_start().starts_with(&prefix))
}
fn line_for_pid(output: &str, pid: u64) -> Option<&str> {
let prefix = format!("{pid} ");
output
.lines()
.find(|line| line.trim_start().starts_with(&prefix))
}
fn find_non_init_child_of_pid_zero() -> Option<u64> {
let proc_dir = fs::read_dir("/proc").ok()?;
for entry in proc_dir {
let entry = entry.ok()?;
let filename = entry.file_name();
let Some(pid) = filename.to_str().and_then(|s| s.parse::<u64>().ok()) else {
continue;
};
if pid == 1 {
continue;
}
let status_path = format!("/proc/{pid}/status");
let Ok(status) = fs::read_to_string(status_path) else {
continue;
};
if status.lines().any(|line| line.trim() == "PPid:\t0") {
return Some(pid);
}
}
None
}
#[test]
fn ptree_shows_parent_and_child_with_arguments() {
let parent_arg = "P";
let child_arg = "C";
let output = common::run_ptool(
"ptree",
&[],
"examples/ptree_parent_child",
&[parent_arg, child_arg],
&[],
true,
);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let lines: Vec<&str> = stdout.lines().collect();
let parent_index = lines
.iter()
.position(|line| line.contains(parent_arg))
.unwrap_or_else(|| panic!("Did not find parent process line in output:\n{stdout}"));
let child_index = lines
.iter()
.position(|line| line.contains("--ch"))
.unwrap_or_else(|| panic!("Did not find child process line in output:\n{stdout}"));
assert!(
parent_index < child_index,
"Expected parent line to be printed before child line:\n{stdout}"
);
let parent_indent = leading_spaces(lines[parent_index]);
let child_indent = leading_spaces(lines[child_index]);
assert!(
child_indent > parent_indent,
"Expected child line to be more indented than parent line:\n{stdout}"
);
}
#[test]
fn ptree_a_includes_children_of_process_zero() {
let Some(kernel_root_pid) = find_non_init_child_of_pid_zero() else {
eprintln!("Skipping: no non-init child of PID 0 found in /proc");
return;
};
let default_output = Command::new(common::find_exec("ptree"))
.stdin(Stdio::null())
.output()
.unwrap();
assert!(default_output.status.success());
let default_stdout = String::from_utf8_lossy(&default_output.stdout);
let all_output = Command::new(common::find_exec("ptree"))
.arg("-a")
.stdin(Stdio::null())
.output()
.unwrap();
assert!(all_output.status.success());
let all_stdout = String::from_utf8_lossy(&all_output.stdout);
assert!(
!output_has_pid_line(&default_stdout, 1),
"Expected default ptree output to exclude PID 1:\n{default_stdout}"
);
assert!(
output_has_pid_line(&all_stdout, 1),
"Expected ptree -a output to include PID 1:\n{all_stdout}"
);
assert!(
!output_has_pid_line(&default_stdout, kernel_root_pid),
"Expected default ptree output to exclude PID {kernel_root_pid}:\n{default_stdout}"
);
assert!(
output_has_pid_line(&all_stdout, kernel_root_pid),
"Expected ptree -a output to include PID {kernel_root_pid}:\n{all_stdout}"
);
let kernel_line = line_for_pid(&all_stdout, kernel_root_pid).unwrap();
let (_, rest) = kernel_line.trim_start().split_once(" ").unwrap();
assert!(
!rest.trim().is_empty(),
"Expected ptree -a to print a process name for PID {kernel_root_pid}:\n{all_stdout}"
);
}
#[test]
fn ptree_accepts_username_operand() {
let Some(username) = current_username() else {
eprintln!("Skipping: unable to resolve current username");
return;
};
let parent_arg = "PU";
let child_arg = "CU";
let output = common::run_ptool(
"ptree",
&[username.as_str(), "__NO_PID__"],
"examples/ptree_parent_child",
&[parent_arg, child_arg],
&[],
true,
);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(
&stdout,
parent_arg,
"Expected ptree <user> output to include spawned parent process",
);
assert_contains(
&stdout,
"--ch",
"Expected ptree <user> output to include spawned child process",
);
}
#[test]
fn ptree_truncates_lines_to_columns_width() {
let columns = "40";
let parent_arg = "TRUNCATION_TEST_PARENT_LONG_ARGUMENT";
let child_arg = "TRUNCATION_TEST_CHILD_LONG_ARGUMENT";
let ready = common::ReadySignal::new(true);
let mut example_cmd = Command::new(common::find_exec("examples/ptree_parent_child"));
example_cmd
.args([parent_arg, child_arg])
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.stdout(Stdio::inherit());
ready.apply_to_command(&mut example_cmd);
let mut child = example_cmd.spawn().expect("failed to spawn example");
ready.wait_for_readiness(&mut child);
let output = Command::new(common::find_exec("ptree"))
.arg(child.id().to_string())
.env("COLUMNS", columns)
.stdin(Stdio::null())
.output()
.expect("failed to run ptree");
let _ = child.kill();
let _ = child.wait();
ready.cleanup();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let max_width: usize = columns.parse().unwrap();
for line in stdout.lines() {
assert!(
line.len() <= max_width,
"Line exceeds {max_width} columns ({} chars): {line:?}",
line.len()
);
}
assert!(
stdout.lines().count() >= 2,
"Expected at least 2 lines of output:\n{stdout}"
);
}
#[test]
fn ptree_wrap_flag_disables_truncation() {
let columns = "40";
let parent_arg = "WRAP_TEST_PARENT_WITH_A_VERY_LONG_ARGUMENT_THAT_EXCEEDS_FORTY_COLUMNS";
let child_arg = "WRAP_TEST_CHILD";
let ready = common::ReadySignal::new(true);
let mut example_cmd = Command::new(common::find_exec("examples/ptree_parent_child"));
example_cmd
.args([parent_arg, child_arg])
.stdin(Stdio::null())
.stderr(Stdio::inherit())
.stdout(Stdio::inherit());
ready.apply_to_command(&mut example_cmd);
let mut child = example_cmd.spawn().expect("failed to spawn example");
ready.wait_for_readiness(&mut child);
let output = Command::new(common::find_exec("ptree"))
.args(["-w", &child.id().to_string()])
.env("COLUMNS", columns)
.stdin(Stdio::null())
.output()
.expect("failed to run ptree");
let _ = child.kill();
let _ = child.wait();
ready.cleanup();
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
let max_width: usize = columns.parse().unwrap();
let has_long_line = stdout.lines().any(|line| line.len() > max_width);
assert!(
has_long_line,
"Expected at least one line to exceed {max_width} columns with -w flag:\n{stdout}"
);
assert_contains(
&stdout,
parent_arg,
"Expected full parent argument to appear with -w flag",
);
}
#[test]
fn ptree_accepts_mixed_pid_and_user_operands() {
let Some(username) = current_username() else {
eprintln!("Skipping: unable to resolve current username");
return;
};
let parent_arg = "PM";
let child_arg = "CM";
let output = common::run_ptool(
"ptree",
&["__PID__", username.as_str()],
"examples/ptree_parent_child",
&[parent_arg, child_arg],
&[],
true,
);
assert!(output.status.success());
let stdout = String::from_utf8_lossy(&output.stdout);
assert_contains(
&stdout,
parent_arg,
"Expected mixed ptree operands output to include spawned parent process",
);
}