use std::process::Command;
fn lsofrs() -> Command {
Command::new(env!("CARGO_BIN_EXE_lsofrs"))
}
#[test]
fn help_flag_short() {
let out = lsofrs().arg("-h").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("FILE DESCRIPTOR SCANNER"));
assert!(stdout.contains("USAGE:"));
assert!(stdout.contains("SELECTION"));
assert!(stdout.contains("NETWORK"));
assert!(stdout.contains("EXAMPLES"));
}
#[test]
fn help_flag_long() {
let out = lsofrs().arg("--help").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("FILE DESCRIPTOR SCANNER"));
}
#[test]
fn version_flag() {
let out = lsofrs().arg("-V").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn version_flag_long() {
let out = lsofrs().arg("--version").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn default_run_produces_output() {
let out = lsofrs().output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("COMMAND"));
assert!(stdout.contains("PID"));
assert!(stdout.contains("USER"));
assert!(stdout.contains("FD"));
assert!(stdout.contains("TYPE"));
assert!(stdout.contains("NAME"));
}
#[test]
fn default_run_has_processes() {
let out = lsofrs().output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert!(
lines.len() > 5,
"expected output lines, got {}",
lines.len()
);
}
#[test]
fn pid_filter_self() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines().skip(1) {
if line.trim().is_empty() {
continue;
}
if !line.starts_with(' ') {
assert!(
line.contains(&my_pid),
"line doesn't contain our pid: {line}"
);
}
}
}
#[test]
fn pid_filter_nonexistent() {
let out = lsofrs().args(["-p", "9999999"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let lines: Vec<&str> = stdout.lines().collect();
assert!(lines.len() <= 1, "expected no data for nonexistent pid");
}
#[test]
fn terse_output_pids_only() {
let out = lsofrs().arg("-t").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
assert!(
line.trim().parse::<i32>().is_ok(),
"terse line should be a PID: '{line}'"
);
}
}
#[test]
fn terse_with_pid_filter() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-t", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let pids: Vec<&str> = stdout.lines().filter(|l| !l.is_empty()).collect();
assert!(!pids.is_empty());
assert!(pids.contains(&my_pid.as_str()));
}
#[test]
fn json_output_valid() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-J", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("JSON output should be valid JSON");
assert!(parsed.is_array());
}
#[test]
fn json_output_has_fields() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["--json", "-p", &my_pid]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
assert!(!parsed.is_empty());
let proc = &parsed[0];
assert!(proc.get("command").is_some());
assert!(proc.get("pid").is_some());
assert!(proc.get("uid").is_some());
assert!(proc.get("files").is_some());
assert_eq!(proc["pid"].as_i64().unwrap(), std::process::id() as i64);
}
#[test]
fn json_files_have_fd_and_name() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-J", "-p", &my_pid]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
let files = parsed[0]["files"].as_array().unwrap();
assert!(!files.is_empty());
for f in files {
assert!(f.get("fd").is_some(), "file missing fd field");
assert!(f.get("name").is_some(), "file missing name field");
assert!(f.get("type").is_some(), "file missing type field");
}
}
#[test]
fn json_wins_when_field_output_also_specified() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-J", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
assert!(!parsed.is_empty());
assert!(parsed[0].get("command").is_some());
}
#[test]
fn csv_output_takes_precedence_over_json() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--csv", "-J", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV header must appear when --csv is combined with -J"
);
}
#[test]
fn csv_output_takes_precedence_over_json_when_json_long_first() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--json", "--csv", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV branch runs before JSON in main even when --json appears first on argv"
);
}
#[test]
fn csv_output_takes_precedence_over_json_when_json_short_first() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-J", "--csv", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV branch runs before JSON in main even when -J appears first on argv"
);
}
#[test]
fn json_takes_precedence_over_terse() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-J", "-t", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert!(parsed.is_array());
}
#[test]
fn json_takes_precedence_over_terse_when_terse_flag_first() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-t", "-J", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert!(parsed.is_array());
}
#[test]
fn json_wins_when_field_output_also_specified_when_field_first() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-F", "pn", "-J", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
assert!(!parsed.is_empty());
assert!(parsed[0].get("command").is_some());
}
#[test]
fn json_long_wins_when_field_output_also_specified() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--json", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
assert!(!parsed.is_empty());
assert!(parsed[0].get("command").is_some());
}
#[test]
fn json_long_wins_when_field_output_when_field_first() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-F", "pn", "--json", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
assert!(!parsed.is_empty());
assert!(parsed[0].get("command").is_some());
}
#[test]
fn terse_takes_precedence_over_field_when_field_flag_first() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-F", "pn", "-t", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
assert!(
line.trim().parse::<i32>().is_ok(),
"terse should run before -F field output; expected PID line, got '{line}'"
);
}
}
#[test]
fn csv_takes_precedence_over_terse() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--csv", "-t", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV branch runs before terse when both flags are set"
);
}
#[test]
fn csv_takes_precedence_over_terse_when_terse_flag_first() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-t", "--csv", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV runs before terse when -t appears first on argv"
);
}
#[test]
fn json_long_takes_precedence_over_terse_when_terse_flag_first() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-t", "--json", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
assert!(parsed.is_array());
}
#[test]
fn stale_takes_precedence_over_terse_when_terse_flag_first() {
let out = lsofrs().args(["-t", "--stale"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"stale runs before terse when -t appears first on argv; got: {stdout}"
);
}
#[test]
fn terse_takes_precedence_over_field_output() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-t", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
assert!(
line.trim().parse::<i32>().is_ok(),
"terse should run before -F field output; expected PID line, got '{line}'"
);
}
}
#[test]
fn stale_takes_precedence_over_terse() {
let out = lsofrs().args(["--stale", "-t"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"stale runs before terse; expected stale banner or empty message, got: {stdout}"
);
}
#[test]
fn tree_json_takes_precedence_over_terse() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--tree", "-t", "-J", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap();
let arr = v.as_array().expect("tree --json should emit a JSON array");
assert!(!arr.is_empty());
assert!(arr[0].get("pid").is_some());
}
#[test]
fn summary_json_takes_precedence_over_terse() {
let out = lsofrs().args(["--summary", "-t", "-J"]).output().unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
assert!(v.get("summary").is_some());
}
#[test]
fn net_map_json_takes_precedence_over_terse() {
let out = lsofrs().args(["--net-map", "-t", "-J"]).output().unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
assert!(v.get("net_map").is_some());
}
#[test]
fn ports_json_takes_precedence_over_terse() {
let out = lsofrs().args(["--ports", "-t", "-J"]).output().unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
assert!(v.get("listening_ports").is_some());
}
#[test]
fn pipe_chain_json_takes_precedence_over_terse() {
let out = lsofrs()
.args(["--pipe-chain", "-t", "-J"])
.output()
.unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
assert!(v.get("pipe_chains").is_some());
}
#[test]
fn csv_takes_precedence_over_summary() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--csv", "--summary", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV runs before --summary when both are set"
);
}
#[test]
fn csv_takes_precedence_over_tree() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--csv", "--tree", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV runs before --tree when both are set"
);
}
#[test]
fn stale_takes_precedence_over_csv() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--stale", "--csv", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.starts_with("COMMAND,"),
"stale runs before CSV export"
);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"expected stale output, got: {stdout}"
);
}
#[test]
fn ports_takes_precedence_over_csv() {
let out = lsofrs().args(["--ports", "--csv"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.starts_with("COMMAND,"),
"ports runs before CSV export"
);
assert!(
stdout.contains("Listening") || stdout.contains("No listening"),
"expected ports banner, got: {stdout}"
);
}
#[test]
fn pipe_chain_takes_precedence_over_csv() {
let out = lsofrs().args(["--pipe-chain", "--csv"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.starts_with("COMMAND,"),
"pipe-chain runs before CSV export"
);
assert!(
stdout.contains("Pipe") || stdout.contains("No pipe"),
"expected pipe-chain banner, got: {stdout}"
);
}
#[test]
fn csv_takes_precedence_over_net_map() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--csv", "--net-map", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV runs before --net-map when both are set"
);
}
#[test]
fn net_map_takes_precedence_over_tree() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--net-map", "--tree", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Network Connection Map") || stdout.contains("No network"),
"net-map runs before tree, expected net-map output"
);
assert!(
!stdout.contains("OPEN FILES"),
"tree output should not appear when net-map wins"
);
}
#[test]
fn tree_takes_precedence_over_summary() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--tree", "--summary", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("OPEN FILES"), "tree runs before summary");
assert!(
!stdout.contains("lsofrs summary"),
"summary text should not appear when tree wins"
);
}
#[test]
fn stale_takes_precedence_over_ports() {
let out = lsofrs().args(["--stale", "--ports"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"expected stale output"
);
assert!(
!stdout.contains("Listening"),
"ports should not run when stale wins"
);
}
#[test]
fn stale_takes_precedence_over_net_map() {
let out = lsofrs().args(["--stale", "--net-map"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"expected stale output"
);
assert!(
!stdout.contains("No network connections"),
"net-map should not run when stale wins"
);
}
#[test]
fn ports_takes_precedence_over_net_map() {
let out = lsofrs().args(["--ports", "--net-map"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Listening") || stdout.contains("No listening"),
"expected ports output"
);
assert!(
!stdout.contains("Network Connection Map") && !stdout.contains("No network connections"),
"net-map should not run when ports wins"
);
}
#[test]
fn ports_takes_precedence_over_pipe_chain() {
let out = lsofrs().args(["--ports", "--pipe-chain"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Listening") || stdout.contains("No listening"),
"expected ports output"
);
assert!(
!stdout.contains("IPC Topology"),
"pipe-chain should not run when ports wins"
);
}
#[test]
fn pipe_chain_takes_precedence_over_net_map() {
let out = lsofrs()
.args(["--pipe-chain", "--net-map"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("IPC Topology") || stdout.contains("No pipe/socket IPC connections"),
"expected pipe-chain output"
);
assert!(
!stdout.contains("Network Connection Map") && !stdout.contains("No network connections"),
"net-map should not run when pipe-chain wins"
);
}
#[test]
fn summary_long_json_is_wrapped_not_default_array() {
let out = lsofrs().args(["--summary", "--json"]).output().unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
let s = v
.get("summary")
.expect("--summary --json should wrap in summary object");
assert!(s.is_object(), "summary value should be an object");
}
#[test]
fn stale_takes_precedence_over_pipe_chain() {
let out = lsofrs().args(["--stale", "--pipe-chain"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"expected stale output"
);
assert!(
!stdout.contains("IPC Topology"),
"pipe-chain should not run when stale wins"
);
}
#[test]
fn net_map_takes_precedence_over_summary() {
let out = lsofrs().args(["--net-map", "--summary"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Network Connection Map") || stdout.contains("No network connections"),
"expected net-map output"
);
assert!(
!stdout.contains("lsofrs summary"),
"summary should not run when net-map wins"
);
}
#[test]
fn tree_json_emits_tree_nodes_not_default_lsof_rows() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--tree", "-J", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
let arr = v.as_array().expect("tree --json should be a JSON array");
assert!(!arr.is_empty());
let first = &arr[0];
assert!(
first.get("children").is_some(),
"tree JSON should expose children[]"
);
assert!(
first.get("files").is_none(),
"default lsof -J rows include files[]; tree JSON should not"
);
}
#[test]
fn stale_takes_precedence_over_tree() {
let out = lsofrs().args(["--stale", "--tree"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"expected stale output"
);
assert!(
!stdout.contains("OPEN FILES"),
"tree should not run when stale wins"
);
}
#[test]
fn stale_takes_precedence_over_summary() {
let out = lsofrs().args(["--stale", "--summary"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"expected stale output"
);
assert!(
!stdout.contains("lsofrs summary"),
"summary should not run when stale wins"
);
}
#[test]
fn ports_takes_precedence_over_tree() {
let out = lsofrs().args(["--ports", "--tree"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Listening") || stdout.contains("No listening"),
"expected ports output"
);
assert!(
!stdout.contains("OPEN FILES"),
"tree should not run when ports wins"
);
}
#[test]
fn ports_takes_precedence_over_summary() {
let out = lsofrs().args(["--ports", "--summary"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Listening") || stdout.contains("No listening"),
"expected ports output"
);
assert!(
!stdout.contains("lsofrs summary"),
"summary should not run when ports wins"
);
}
#[test]
fn pipe_chain_takes_precedence_over_tree() {
let out = lsofrs().args(["--pipe-chain", "--tree"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("IPC Topology") || stdout.contains("No pipe/socket IPC connections"),
"expected pipe-chain output"
);
assert!(
!stdout.contains("OPEN FILES"),
"tree should not run when pipe-chain wins"
);
}
#[test]
fn pipe_chain_takes_precedence_over_summary() {
let out = lsofrs()
.args(["--pipe-chain", "--summary"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("IPC Topology") || stdout.contains("No pipe/socket IPC connections"),
"expected pipe-chain output"
);
assert!(
!stdout.contains("lsofrs summary"),
"summary should not run when pipe-chain wins"
);
}
#[test]
fn net_map_takes_precedence_over_terse_text() {
let out = lsofrs().args(["--net-map", "-t"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Network Connection Map") || stdout.contains("No network connections"),
"net-map should run before terse"
);
}
#[test]
fn net_map_takes_precedence_over_terse_text_when_terse_flag_first() {
let out = lsofrs().args(["-t", "--net-map"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Network Connection Map") || stdout.contains("No network connections"),
"net-map should run before terse when -t appears first on argv"
);
}
#[test]
fn summary_takes_precedence_over_terse_text() {
let out = lsofrs().args(["--summary", "-t"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Processes:") && stdout.contains("Open files:"));
}
#[test]
fn summary_takes_precedence_over_terse_text_when_terse_flag_first() {
let out = lsofrs().args(["-t", "--summary"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Processes:") && stdout.contains("Open files:"));
}
#[test]
fn tree_takes_precedence_over_terse_text() {
let out = lsofrs().args(["--tree", "-t"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("OPEN FILES"),
"tree should run before terse"
);
}
#[test]
fn tree_takes_precedence_over_terse_text_when_terse_flag_first() {
let out = lsofrs().args(["-t", "--tree"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("OPEN FILES"),
"tree should run before terse when -t appears first on argv"
);
}
#[test]
fn stale_json_takes_precedence_over_terse() {
let out = lsofrs().args(["--stale", "-J", "-t"]).output().unwrap();
assert!(out.status.success());
let v: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&out.stdout)).unwrap();
assert!(v.get("stale_fds").is_some());
}
#[test]
fn ports_takes_precedence_over_terse_text() {
let out = lsofrs().args(["--ports", "-t"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Listening") || stdout.contains("No listening"),
"ports should run before terse"
);
}
#[test]
fn ports_takes_precedence_over_terse_text_when_terse_flag_first() {
let out = lsofrs().args(["-t", "--ports"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Listening") || stdout.contains("No listening"),
"ports should run before terse when -t appears first on argv"
);
}
#[test]
fn pipe_chain_takes_precedence_over_terse_text() {
let out = lsofrs().args(["--pipe-chain", "-t"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("IPC Topology") || stdout.contains("No pipe/socket IPC connections"),
"pipe-chain should run before terse"
);
}
#[test]
fn pipe_chain_takes_precedence_over_terse_text_when_terse_flag_first() {
let out = lsofrs().args(["-t", "--pipe-chain"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("IPC Topology") || stdout.contains("No pipe/socket IPC connections"),
"pipe-chain should run before terse when -t appears first on argv"
);
}
#[test]
fn csv_takes_precedence_over_field_output() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--csv", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV runs before -F field output"
);
}
#[test]
fn csv_takes_precedence_over_field_output_when_field_first_on_argv() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-F", "pn", "--csv", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.starts_with("COMMAND,"),
"CSV runs before -F field output even when -F appears first on argv"
);
}
#[test]
fn stale_takes_precedence_over_field_output() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--stale", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("No stale") || stdout.contains("Stale FD"),
"stale runs before -F"
);
assert!(
!stdout.contains(&format!("p{my_pid}")),
"field tokens should not appear when stale wins"
);
}
#[test]
fn ports_takes_precedence_over_field_output() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--ports", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Listening") || stdout.contains("No listening"),
"ports runs before -F"
);
assert!(
!stdout.contains(&format!("p{my_pid}")),
"field tokens should not appear when ports wins"
);
}
#[test]
fn pipe_chain_takes_precedence_over_field_output() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--pipe-chain", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("IPC Topology") || stdout.contains("No pipe/socket IPC connections"),
"pipe-chain runs before -F"
);
assert!(
!stdout.contains(&format!("p{my_pid}")),
"field tokens should not appear when pipe-chain wins"
);
}
#[test]
fn net_map_takes_precedence_over_field_output() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--net-map", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("Network Connection Map") || stdout.contains("No network connections"),
"net-map runs before -F"
);
assert!(
!stdout.contains(&format!("p{my_pid}")),
"field tokens should not appear when net-map wins"
);
}
#[test]
fn tree_takes_precedence_over_field_output() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--tree", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("OPEN FILES"), "tree runs before -F");
assert!(
!stdout.contains(&format!("p{my_pid}")),
"field tokens should not appear when tree wins"
);
}
#[test]
fn summary_takes_precedence_over_field_output() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--summary", "-F", "pn", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("Processes:") && stdout.contains("Open files:"));
assert!(
!stdout.contains(&format!("p{my_pid}")),
"field tokens should not appear when summary wins"
);
}
#[test]
fn field_output_pid_and_name() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-F", "pn", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains(&format!("p{my_pid}")));
assert!(stdout.lines().any(|l| l.starts_with('n')));
}
#[test]
fn field_output_nul_terminator() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-F", "p", "-0", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = &out.stdout;
assert!(stdout.contains(&0u8), "NUL terminator should be present");
}
#[test]
fn command_filter_prefix() {
let out = lsofrs().args(["-c", "lsofrs"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_flag_no_crash() {
let out = lsofrs().arg("-i").output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_tcp_no_crash() {
let out = lsofrs().args(["-i", "TCP"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_udp_no_crash() {
let out = lsofrs().args(["-i", "UDP"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_port_no_crash() {
let out = lsofrs().args(["-i", ":443"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_4_no_crash() {
let out = lsofrs().args(["-i", "4"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_6_no_crash() {
let out = lsofrs().args(["-i", "6"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_4tcp_no_crash() {
let out = lsofrs().args(["-i", "4TCP"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn unix_socket_flag_no_crash() {
let out = lsofrs().arg("-U").output().unwrap();
assert!(out.status.success());
}
#[test]
fn nfs_flag_no_crash() {
let out = lsofrs().arg("-N").output().unwrap();
assert!(out.status.success());
}
#[test]
fn summary_mode() {
let out = lsofrs().arg("--summary").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("FD") || stdout.contains("summary") || stdout.contains("TYPE"));
}
#[test]
fn summary_json() {
let out = lsofrs().args(["--summary", "--json"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("summary --json should produce valid JSON");
assert!(parsed.is_object() || parsed.is_array());
}
#[test]
fn fd_filter_range() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-d", "0-2", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn fd_filter_cwd() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-d", "cwd", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn and_mode_narrows_results() {
let my_pid = std::process::id().to_string();
let out_or = lsofrs()
.args(["-p", &my_pid, "-c", "nonexistent_cmd"])
.output()
.unwrap();
let count_or = String::from_utf8_lossy(&out_or.stdout).lines().count();
let out_and = lsofrs()
.args(["-a", "-p", &my_pid, "-c", "nonexistent_cmd"])
.output()
.unwrap();
let count_and = String::from_utf8_lossy(&out_and.stdout).lines().count();
assert!(
count_and < count_or,
"AND mode should produce fewer results"
);
}
#[test]
fn pgid_show_flag() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--pgid-show", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("PGID") || stdout.contains("PG/ID"));
}
#[test]
fn ppid_show_flag() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-R", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("PPID") || stdout.contains("P/PID"));
}
#[test]
fn pid_exclude() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-p", &format!("^{my_pid}")])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines().skip(1) {
if line.starts_with(' ') || line.trim().is_empty() {
continue;
}
assert!(
!line.contains(&format!(" {my_pid} ")),
"excluded pid should not appear: {line}"
);
}
}
#[test]
fn file_arg_no_crash() {
let out = lsofrs().arg("/dev/null").output().unwrap();
assert!(out.status.success());
}
#[test]
fn combined_flags_no_crash() {
let out = lsofrs().args(["-n", "-P", "-w"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn tree_mode() {
let out = lsofrs().arg("--tree").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("PID"));
assert!(stdout.contains("FDs"));
assert!(stdout.contains("CMD"));
}
#[test]
fn tree_mode_with_pid_filter() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["--tree", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains(&my_pid));
}
#[test]
fn tree_mode_json() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--tree", "--json", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("tree --json should produce valid JSON");
assert!(parsed.is_array());
let arr = parsed.as_array().unwrap();
assert!(!arr.is_empty());
assert!(arr[0].get("pid").is_some());
assert!(arr[0].get("fd_count").is_some());
assert!(arr[0].get("children").is_some());
}
#[test]
fn tree_mode_with_command_filter() {
let out = lsofrs().args(["--tree", "-c", "lsofrs"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn tree_mode_with_user_filter() {
let out = lsofrs().args(["--tree", "-u", &whoami()]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("PID"));
}
#[test]
fn json_empty_result_valid() {
let out = lsofrs().args(["-J", "-p", "9999999"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
assert!(parsed.is_empty());
}
#[test]
fn json_file_has_type_field() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-J", "-p", &my_pid]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
if !parsed.is_empty() {
let files = parsed[0]["files"].as_array().unwrap();
if !files.is_empty() {
let t = files[0]["type"].as_str().unwrap();
assert!(!t.is_empty());
}
}
}
#[test]
fn field_output_command_field() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-F", "pc", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.lines().any(|l| l.starts_with('c')));
}
#[test]
fn field_output_type_field() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-F", "pt", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.lines().any(|l| l.starts_with('t')));
}
#[test]
fn field_output_uid_field() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-F", "pu", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.lines().any(|l| l.starts_with('u')));
}
#[test]
fn field_output_login_name_field() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-F", "pL", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.lines().any(|l| l.starts_with('L')));
}
#[test]
fn pid_and_user_filter_or() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-p", &my_pid, "-u", &whoami()])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.lines().count() > 1);
}
#[test]
fn exclude_pid_still_shows_others() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-p", &format!("^{my_pid}")])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.lines().count() > 1, "should have other processes");
}
#[test]
fn fd_filter_single_number() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-d", "0", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn fd_filter_exclude_range() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-d", "^0-2", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn inet_tcp_port_combo() {
let out = lsofrs().args(["-i", "TCP:443"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_6tcp_combo() {
let out = lsofrs().args(["-i", "6TCP"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_4udp_combo() {
let out = lsofrs().args(["-i", "4UDP"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn inet_host_port_combo() {
let out = lsofrs().args(["-i", "TCP@127.0.0.1:80"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn summary_with_pid_filter() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--summary", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn summary_with_command_filter() {
let out = lsofrs()
.args(["--summary", "-c", "lsofrs"])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn terse_no_header() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["-t", "-p", &my_pid]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(!stdout.contains("COMMAND"));
assert!(!stdout.contains("PID"));
}
#[test]
fn terse_nonexistent_pid_empty() {
let out = lsofrs().args(["-t", "-p", "9999999"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.trim().is_empty());
}
#[test]
fn all_boolean_flags_no_crash() {
let out = lsofrs()
.args(["-n", "-P", "-w", "-R", "--pgid-show"])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn json_with_pgid_ppid() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["-J", "-R", "--pgid-show", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: Vec<serde_json::Value> = serde_json::from_str(&stdout).unwrap();
assert!(!parsed.is_empty());
assert!(parsed[0].get("ppid").is_some());
assert!(parsed[0].get("pgid").is_some());
}
#[test]
fn stale_mode_no_crash() {
let out = lsofrs().arg("--stale").output().unwrap();
assert!(out.status.success());
}
#[test]
fn stale_mode_with_user() {
let out = lsofrs()
.args(["--stale", "-u", &whoami()])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn stale_mode_json() {
let out = lsofrs().args(["--stale", "--json"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("stale --json should be valid JSON");
assert!(parsed.is_object() || parsed.is_array());
}
#[test]
fn stale_mode_json_with_pid() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--stale", "--json", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let _: serde_json::Value = serde_json::from_str(&stdout).unwrap();
}
#[test]
fn ports_mode_no_crash() {
let out = lsofrs().arg("--ports").output().unwrap();
assert!(out.status.success());
}
#[test]
fn ports_mode_has_output() {
let out = lsofrs().arg("--ports").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("PROTO") || stdout.contains("PORT") || stdout.contains("No listening"),
"ports output should have header or 'no ports' message"
);
}
#[test]
fn ports_mode_format_correct() {
let out = lsofrs().arg("--ports").output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
if stdout.contains("PROTO") {
let data_lines: Vec<&str> = stdout
.lines()
.filter(|l| l.contains("TCP") || l.contains("UDP"))
.collect();
for line in &data_lines {
assert!(
line.contains("TCP") || line.contains("UDP"),
"port data row should contain protocol: {line}"
);
}
}
}
#[test]
fn ports_json_has_port_field() {
let out = lsofrs().args(["--ports", "--json"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
if let Some(ports) = parsed.get("listening_ports").and_then(|v| v.as_array()) {
for port in ports {
assert!(
port.get("port").is_some(),
"port entry missing 'port' field"
);
assert!(
port.get("protocol").is_some() || port.get("proto").is_some(),
"port entry missing protocol field"
);
assert!(port.get("pid").is_some(), "port entry missing 'pid' field");
}
}
}
#[test]
fn inet_tcp_shows_state() {
let out = lsofrs().args(["-i", "TCP"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let data_lines: Vec<&str> = stdout
.lines()
.skip(1)
.filter(|l| !l.trim().is_empty())
.collect();
if data_lines.len() > 2 {
let has_state = data_lines
.iter()
.any(|l| l.contains("LISTEN") || l.contains("ESTABLISHED") || l.contains("CLOSE"));
let has_tcp = data_lines.iter().any(|l| l.contains("TCP"));
if has_tcp {
assert!(
has_state,
"TCP connections should have state (LISTEN, ESTABLISHED, etc.)"
);
}
}
}
#[test]
fn ports_mode_with_user() {
let out = lsofrs()
.args(["--ports", "-u", &whoami()])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn ports_mode_json() {
let out = lsofrs().args(["--ports", "--json"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value =
serde_json::from_str(&stdout).expect("ports --json should be valid JSON");
assert!(parsed.is_object() || parsed.is_array());
}
#[test]
fn ports_mode_json_with_pid() {
let my_pid = std::process::id().to_string();
let out = lsofrs()
.args(["--ports", "--json", "-p", &my_pid])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn watch_mode_no_crash() {
let out = lsofrs().args(["--watch", "/dev/null"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn watch_mode_nonexistent_file() {
let out = lsofrs()
.args(["--watch", "/nonexistent/path/xyz"])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn help_contains_stale_and_ports() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--stale"));
assert!(stdout.contains("--ports"));
assert!(stdout.contains("--watch"));
assert!(stdout.contains("--top"));
}
#[test]
fn help_contains_all_sections() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("SELECTION"));
assert!(stdout.contains("NETWORK"));
assert!(stdout.contains("FILES & DIRECTORIES"));
assert!(stdout.contains("DISPLAY"));
assert!(stdout.contains("SYSTEM"));
assert!(stdout.contains("EXAMPLES"));
assert!(stdout.contains("INFO"));
}
#[test]
fn help_contains_tree_option() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--tree"));
}
#[test]
fn help_contains_lsofrs_banner() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("FILE DESCRIPTOR SCANNER"));
assert!(stdout.contains("Every open file tells a story"));
}
#[test]
fn invalid_flag_exits_nonzero() {
let out = lsofrs().arg("--nonexistent-flag-xyz").output().unwrap();
assert!(!out.status.success());
}
#[test]
fn invalid_short_flag_exits_nonzero() {
let out = lsofrs().arg("-Z").output().unwrap();
assert!(!out.status.success());
}
#[test]
fn csv_mode_no_crash() {
let out = lsofrs().arg("--csv").output().unwrap();
assert!(out.status.success());
}
#[test]
fn csv_mode_has_header() {
let out = lsofrs().arg("--csv").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.starts_with("COMMAND,PID,USER,FD,TYPE,DEVICE,SIZE/OFF,NODE,NAME"));
}
#[test]
fn csv_mode_with_pid() {
let my_pid = std::process::id().to_string();
let out = lsofrs().args(["--csv", "-p", &my_pid]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.lines().count() >= 2);
}
#[test]
fn csv_mode_with_tcp() {
let out = lsofrs().args(["--csv", "-i", "TCP"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn pipe_chain_no_crash() {
let out = lsofrs().arg("--pipe-chain").output().unwrap();
assert!(out.status.success());
}
#[test]
fn pipe_chain_json() {
let out = lsofrs().args(["--pipe-chain", "--json"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let _: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
}
#[test]
fn pipe_chain_with_user() {
let out = lsofrs()
.args(["--pipe-chain", "-u", &whoami()])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn net_map_no_crash() {
let out = lsofrs().arg("--net-map").output().unwrap();
assert!(out.status.success());
}
#[test]
fn net_map_json() {
let out = lsofrs().args(["--net-map", "--json"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let _: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
}
#[test]
fn net_map_with_user() {
let out = lsofrs()
.args(["--net-map", "-u", &whoami()])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn dir_flag_no_crash() {
let out = lsofrs().args(["--dir", "/tmp"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn dir_recurse_flag_no_crash() {
let out = lsofrs().args(["--dir-recurse", "/tmp"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn dir_flag_with_dev() {
let out = lsofrs().args(["--dir", "/dev"]).output().unwrap();
assert!(out.status.success());
}
#[test]
fn dir_recurse_with_json() {
let out = lsofrs()
.args(["--dir-recurse", "/dev", "--json"])
.output()
.unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let _: serde_json::Value = serde_json::from_str(&stdout).expect("valid JSON");
}
#[test]
fn tui_flag_non_tty() {
let out = lsofrs().arg("--tui").output().unwrap();
assert!(out.status.success() || !out.status.success()); }
#[test]
fn theme_flag_classic() {
let out = lsofrs()
.args(["--theme", "classic", "--summary"])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn theme_flag_matrix() {
let out = lsofrs()
.args(["--theme", "matrix", "--summary"])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn theme_flag_unknown_defaults() {
let out = lsofrs()
.args(["--theme", "nonexistent", "--summary"])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn help_contains_tui() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--tui"));
}
#[test]
fn help_contains_dir_flags() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--dir"));
assert!(stdout.contains("--dir-recurse"));
}
#[test]
fn help_contains_csv() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--csv"));
}
#[test]
fn help_contains_pipe_chain() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--pipe-chain"));
}
#[test]
fn help_contains_net_map() {
let out = lsofrs().arg("-h").output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(stdout.contains("--net-map"));
}
#[test]
fn stale_json_has_stale_fds_key() {
let out = lsofrs().args(["--stale", "--json"]).output().unwrap();
let stdout = String::from_utf8_lossy(&out.stdout);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(parsed.get("stale_fds").is_some());
}
#[test]
fn json_with_dir_recurse() {
let out = lsofrs()
.args(["--json", "--dir-recurse", "/dev"])
.output()
.unwrap();
assert!(out.status.success());
}
#[test]
fn terse_with_dir() {
let out = lsofrs().args(["-t", "--dir", "/dev"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
for line in stdout.lines() {
if line.trim().is_empty() {
continue;
}
assert!(
line.trim().parse::<i32>().is_ok(),
"terse should be PID: '{line}'"
);
}
}
#[test]
fn help_contains_leak_detect() {
let out = lsofrs().arg("-h").output().unwrap();
assert!(out.status.success());
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("--leak-detect"));
}
#[test]
fn help_contains_monitor_short_or_long() {
let out = lsofrs().arg("-h").output().unwrap();
assert!(out.status.success());
let s = String::from_utf8_lossy(&out.stdout);
assert!(
s.contains("--monitor") && s.contains("-W"),
"help should document monitor short and long flags"
);
}
#[test]
fn help_contains_follow() {
let out = lsofrs().arg("-h").output().unwrap();
assert!(out.status.success());
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("--follow"));
}
#[test]
fn help_contains_top_mode() {
let out = lsofrs().arg("-h").output().unwrap();
assert!(out.status.success());
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("--top"));
}
#[test]
fn help_contains_stale() {
let out = lsofrs().arg("-h").output().unwrap();
assert!(out.status.success());
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("--stale"));
}
#[test]
fn help_contains_ports() {
let out = lsofrs().arg("-h").output().unwrap();
assert!(out.status.success());
let s = String::from_utf8_lossy(&out.stdout);
assert!(s.contains("--ports"));
}
#[test]
fn json_tcp_filter_is_valid_array() {
let out = lsofrs().args(["-J", "-i", "TCP"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(v.is_array());
}
#[test]
fn json_udp_filter_is_valid_array() {
let out = lsofrs().args(["--json", "-i", "UDP"]).output().unwrap();
assert!(out.status.success());
let stdout = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value = serde_json::from_str(&stdout).unwrap();
assert!(v.is_array());
}
fn whoami() -> String {
String::from_utf8(
std::process::Command::new("whoami")
.output()
.unwrap()
.stdout,
)
.unwrap()
.trim()
.to_string()
}