use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::PathBuf,
};
use pxh::helpers;
use tempfile::NamedTempFile;
fn verify_file_matches(path: &PathBuf, expected_contents: &str) {
let fh = File::open(path).unwrap();
let reader = BufReader::new(fh);
let file_lines: Vec<_> = reader.lines().collect::<Result<Vec<_>, _>>().unwrap();
let expected_lines: Vec<_> = expected_contents.split('\n').collect();
assert_eq!(file_lines, expected_lines);
}
#[test]
fn atomic_line_remove() {
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\nline3\n").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_lines_from_file(&path, "line2").unwrap();
verify_file_matches(&path, "line1\nline3");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\nline3").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_lines_from_file(&path, "line2").unwrap();
verify_file_matches(&path, "line1\nline3");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\nline3").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_lines_from_file(&path, "line9").unwrap();
verify_file_matches(&path, "line1\nline2\nline3");
}
#[test]
fn test_determine_is_pxhs() {
assert!(!helpers::determine_is_pxhs(&["/usr/bin/pxh".to_string()]));
assert!(!helpers::determine_is_pxhs(&["/path/to/pxh".to_string()]));
assert!(!helpers::determine_is_pxhs(&["./pxh".to_string()]));
assert!(helpers::determine_is_pxhs(&["/usr/bin/pxhs".to_string()]));
assert!(helpers::determine_is_pxhs(&["/path/to/pxhs".to_string()]));
assert!(helpers::determine_is_pxhs(&["./pxhs".to_string()]));
assert!(helpers::determine_is_pxhs(&["pxhs".to_string()]));
assert!(helpers::determine_is_pxhs(&["pxhs-something".to_string()]));
assert!(!helpers::determine_is_pxhs(&["".to_string()]));
assert!(!helpers::determine_is_pxhs(&[]));
}
#[test]
fn test_build_remote_pxh_command_explicit_path() {
let cmd = helpers::build_remote_pxh_command(
"/usr/bin/custom-pxh",
"--db ~/.pxh/pxh.db sync --server",
);
assert_eq!(cmd, "/usr/bin/custom-pxh --db ~/.pxh/pxh.db sync --server");
let cmd = helpers::build_remote_pxh_command("custom-pxh", "--db ~/.pxh/pxh.db sync --server");
assert_eq!(cmd, "custom-pxh --db ~/.pxh/pxh.db sync --server");
}
#[test]
fn test_build_remote_pxh_command_default() {
let cmd = helpers::build_remote_pxh_command("pxh", "--db ~/.pxh/pxh.db sync --server");
assert!(cmd.starts_with("sh -c '"), "expected sh -c wrapper, got: {cmd}");
assert!(cmd.contains(".cargo/bin/pxh"), "should probe .cargo/bin");
assert!(cmd.contains("/usr/local/bin/pxh"), "should probe /usr/local/bin");
assert!(cmd.contains("not found on remote host"), "should have fallback error");
assert!(cmd.contains("--db ~/.pxh/pxh.db sync --server"));
}
#[test]
fn test_sqlite_connection_creates_parent_dirs() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("nested").join("subdir").join("pxh.db");
assert!(!db_path.parent().unwrap().exists());
let conn = pxh::sqlite_connection(&Some(db_path.clone()));
assert!(conn.is_ok(), "should create parent dirs: {:?}", conn.err());
assert!(db_path.exists());
}
#[test]
fn test_sqlite_connection_creates_dirs_through_symlink() {
let dir = tempfile::tempdir().unwrap();
let real_target = dir.path().join("real_pxh_dir");
let symlink = dir.path().join("pxh_link");
std::os::unix::fs::symlink(&real_target, &symlink).unwrap();
assert!(!real_target.exists());
let db_path = symlink.join("pxh.db");
let conn = pxh::sqlite_connection(&Some(db_path.clone()));
assert!(conn.is_ok(), "should create target dir through symlink: {:?}", conn.err());
assert!(real_target.exists());
assert!(db_path.exists());
}
#[test]
fn test_get_relative_path_from_home() {
let result = helpers::get_relative_path_from_home(None, None);
match result {
Some(path) => {
assert!(path.is_empty() || !path.is_empty());
}
None => assert!(true), }
let exe = PathBuf::from("/home/user/bin/pxh");
let home = PathBuf::from("/home/user");
let result = helpers::get_relative_path_from_home(Some(&exe), Some(&home));
assert_eq!(result, Some("bin/pxh".to_string()));
}
#[test]
fn test_parse_ssh_command() {
let (cmd, args) = helpers::parse_ssh_command("ssh");
assert_eq!(cmd, "ssh");
assert!(args.is_empty());
let (cmd, args) = helpers::parse_ssh_command("ssh -p 2222");
assert_eq!(cmd, "ssh");
assert_eq!(args, vec!["-p", "2222"]);
let (cmd, args) = helpers::parse_ssh_command("ssh -o 'UserKnownHostsFile=/dev/null'");
assert_eq!(cmd, "ssh");
assert_eq!(args, vec!["-o", "UserKnownHostsFile=/dev/null"]);
let (cmd, args) = helpers::parse_ssh_command("ssh -i \"/path/to/key\"");
assert_eq!(cmd, "ssh");
assert_eq!(args, vec!["-i", "/path/to/key"]);
let (cmd, args) =
helpers::parse_ssh_command("ssh -p 2222 -o 'StrictHostKeyChecking=no' -i /path/to/key");
assert_eq!(cmd, "ssh");
assert_eq!(args, vec!["-p", "2222", "-o", "StrictHostKeyChecking=no", "-i", "/path/to/key"]);
let (cmd, args) = helpers::parse_ssh_command("ssh -o \"Option=\\\"value\\\"\"");
assert_eq!(cmd, "ssh");
assert_eq!(args, vec!["-o", "Option=\"value\""]);
}
#[test]
fn test_path_resolution_across_home_dirs() {
let exe_path1 = PathBuf::from("/Users/chip/bin/pxh");
let home1 = PathBuf::from("/Users/chip");
let result1 = helpers::get_relative_path_from_home(Some(&exe_path1), Some(&home1));
assert_eq!(result1, Some("bin/pxh".to_string()));
let exe_path2 = PathBuf::from("/home/chip/bin/pxh");
let home2 = PathBuf::from("/home/chip");
let result2 = helpers::get_relative_path_from_home(Some(&exe_path2), Some(&home2));
assert_eq!(result2, Some("bin/pxh".to_string()));
assert_eq!(result1, result2);
let exe_outside = PathBuf::from("/usr/bin/pxh");
let result3 = helpers::get_relative_path_from_home(Some(&exe_outside), Some(&home1));
assert_eq!(result3, None);
let same_path = PathBuf::from("/home/chip");
let result4 = helpers::get_relative_path_from_home(Some(&same_path), Some(&same_path));
assert_eq!(result4, Some("".to_string()));
}
#[test]
fn atomic_matching_line_remove() {
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\nline3\n").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_matching_lines_from_file(&path, &["line2"]).unwrap();
verify_file_matches(&path, "line1\nline3");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\nline3\nline4\n").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_matching_lines_from_file(&path, &["line2", "line4"]).unwrap();
verify_file_matches(&path, "line1\nline3");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\n line2 \nline3\n").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_matching_lines_from_file(&path, &["line2"]).unwrap();
verify_file_matches(&path, "line1\nline3");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\nline3\n").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_matching_lines_from_file(&path, &["line9"]).unwrap();
verify_file_matches(&path, "line1\nline2\nline3");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\n").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_matching_lines_from_file(&path, &[]).unwrap();
verify_file_matches(&path, "line1\nline2");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\nline3\n").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_matching_lines_from_file(&path, &["line"]).unwrap();
verify_file_matches(&path, "line1\nline2\nline3");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\nline3").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_matching_lines_from_file(&path, &["line2"]).unwrap();
verify_file_matches(&path, "line1\nline3");
let mut tmpfile = NamedTempFile::new().unwrap();
write!(tmpfile, "line1\nline2\n").unwrap();
let (_, path) = tmpfile.keep().unwrap();
pxh::atomically_remove_matching_lines_from_file(&path, &["line1", "line2"]).unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert!(contents.is_empty());
}
#[test]
fn test_get_hostname_strips_domain() {
use bstr::BString;
use std::env;
let original = env::var_os("PXH_HOSTNAME");
unsafe { env::set_var("PXH_HOSTNAME", "myhost.example.local") };
let hostname = pxh::get_hostname();
assert_eq!(hostname, BString::from("myhost"));
unsafe { env::set_var("PXH_HOSTNAME", "myhost") };
let hostname = pxh::get_hostname();
assert_eq!(hostname, BString::from("myhost"));
unsafe { env::set_var("PXH_HOSTNAME", "host.sub.example.com") };
let hostname = pxh::get_hostname();
assert_eq!(hostname, BString::from("host"));
unsafe { env::set_var("PXH_HOSTNAME", "") };
let hostname = pxh::get_hostname();
assert_eq!(hostname, BString::from(""));
unsafe { env::set_var("PXH_HOSTNAME", ".example.com") };
let hostname = pxh::get_hostname();
assert_eq!(hostname, BString::from(""));
match original {
Some(val) => unsafe { env::set_var("PXH_HOSTNAME", val) },
None => unsafe { env::remove_var("PXH_HOSTNAME") },
}
}