use lsofrs::cli::Args;
use lsofrs::filter::{Filter, parse_inet_filter};
use lsofrs::types::{Access, FdFilter, FdName, FileType, InetAddr, OpenFile, SocketInfo};
use std::process::Command;
fn lsofrs_bin() -> Command {
Command::new(env!("CARGO_BIN_EXE_lsofrs"))
}
#[test]
fn test_fd_filter_named_cwd_matches_cwd_fdname() {
let args = Args::parse_from(["lsofrs", "-d", "cwd"]);
let f = Filter::from_args(&args);
let mut file = OpenFile {
fd: FdName::Cwd,
access: Access::None,
file_type: FileType::Dir,
name: "/tmp".to_string(),
..Default::default()
};
assert!(
f.matches_file(&file),
"named `cwd` FD filter must match an FdName::Cwd file"
);
file.fd = FdName::Txt;
assert!(
!f.matches_file(&file),
"named `cwd` FD filter must NOT match an FdName::Txt file"
);
}
#[test]
fn test_fd_exclude_with_numeric_inverts_match() {
let args = Args::parse_from(["lsofrs", "-d", "^3"]);
let f = Filter::from_args(&args);
let excluded = OpenFile {
fd: FdName::Number(3),
access: Access::Read,
file_type: FileType::Reg,
name: "/x".to_string(),
..Default::default()
};
let passing = OpenFile {
fd: FdName::Number(5),
access: Access::Read,
file_type: FileType::Reg,
name: "/y".to_string(),
..Default::default()
};
assert!(!f.matches_file(&excluded), "fd 3 must be excluded");
assert!(
f.matches_file(&passing),
"fd 5 must pass when only 3 excluded"
);
}
#[test]
fn test_network_filter_port_range_inclusive_endpoints() {
let mut f = Filter::default();
f.network = true;
parse_inet_filter("TCP:100", &mut f);
f.network_filters[0].port_end = Some(200);
let mk = |port: u16| OpenFile {
fd: FdName::Number(0),
access: Access::None,
file_type: FileType::IPv4,
name: format!("*:{port}"),
socket_info: Some(SocketInfo {
local: InetAddr { addr: None, port },
foreign: InetAddr::default(),
protocol: "TCP".to_string(),
..Default::default()
}),
..Default::default()
};
assert!(
f.matches_file(&mk(100)),
"port 100 (lo endpoint) must match"
);
assert!(f.matches_file(&mk(150)), "port 150 (interior) must match");
assert!(
f.matches_file(&mk(200)),
"port 200 (hi endpoint) must match"
);
assert!(
!f.matches_file(&mk(99)),
"port 99 (below lo) must NOT match"
);
assert!(
!f.matches_file(&mk(201)),
"port 201 (above hi) must NOT match"
);
}
#[test]
fn test_parse_fd_filter_non_numeric_creates_name_variant() {
let args = Args::parse_from(["lsofrs", "-d", "rtd"]);
let f = Filter::from_args(&args);
assert_eq!(
f.fd_filters.len(),
1,
"exactly one fd filter expected; got {}",
f.fd_filters.len()
);
match &f.fd_filters[0] {
FdFilter::Name(s) => assert_eq!(s, "rtd"),
other => panic!("expected FdFilter::Name(\"rtd\"), got {other:?}"),
}
}
#[test]
fn test_fd_range_dash_form_matches_both_endpoints() {
let args = Args::parse_from(["lsofrs", "-d", "5-9"]);
let f = Filter::from_args(&args);
for n in [5, 7, 9] {
let file = OpenFile {
fd: FdName::Number(n),
access: Access::None,
file_type: FileType::Reg,
name: "/x".to_string(),
..Default::default()
};
assert!(
f.matches_file(&file),
"fd {n} must match range 5-9 (inclusive)"
);
}
for n in [4, 10] {
let file = OpenFile {
fd: FdName::Number(n),
access: Access::None,
file_type: FileType::Reg,
name: "/x".to_string(),
..Default::default()
};
assert!(
!f.matches_file(&file),
"fd {n} must NOT match range 5-9 (outside)"
);
}
}
#[test]
fn test_csv_end_to_end_header_columns_unchanged() {
let out = lsofrs_bin()
.args(["-p", &std::process::id().to_string(), "--csv"])
.output()
.expect("spawn lsofrs --csv");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let header = stdout.lines().next().unwrap_or("<empty>").to_string();
assert_eq!(
header, "COMMAND,PID,USER,FD,TYPE,DEVICE,SIZE/OFF,NODE,NAME",
"CSV header must remain RFC 4180 contract; got {header:?}"
);
}
#[test]
fn test_nfs_only_flag_rejects_regular_accepts_nfs() {
let args = Args::parse_from(["lsofrs", "-N"]);
let f = Filter::from_args(&args);
let reg = OpenFile {
fd: FdName::Number(3),
access: Access::Read,
file_type: FileType::Reg,
name: "/local/file".to_string(),
is_nfs: false,
..Default::default()
};
let nfs = OpenFile {
fd: FdName::Number(4),
access: Access::Read,
file_type: FileType::Reg,
name: "/mnt/nfs/file".to_string(),
is_nfs: true,
..Default::default()
};
assert!(!f.matches_file(®), "-N must reject non-NFS regular file");
assert!(f.matches_file(&nfs), "-N must accept NFS-flagged file");
}
#[test]
fn test_leak_detect_params_none_when_unset_some_when_set() {
let args_none = Args::parse_from(["lsofrs"]);
assert!(
args_none.leak_detect_params().is_none(),
"leak_detect_params must be None without --leak-detect flag"
);
let args_some = Args::parse_from(["lsofrs", "--leak-detect=10,5"]);
let (interval, threshold) = args_some
.leak_detect_params()
.expect("--leak-detect=10,5 must yield Some");
assert_eq!(interval, 10, "interval must parse as 10");
assert_eq!(threshold, 5, "threshold must parse as 5");
}