#[cfg(test)]
mod test_python_inspired {
use crate::soft_canonicalize;
use std::sync::Mutex;
static WORKING_DIR_MUTEX: Mutex<()> = Mutex::new(());
use std::{env, fs};
use tempfile::TempDir;
#[test]
fn test_resolve_nonexist_relative() {
let _lock = WORKING_DIR_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let original_cwd = env::current_dir().unwrap();
let test_result = std::panic::catch_unwind(|| {
if base_path.exists() {
if let Ok(()) = env::set_current_dir(base_path) {
let result = soft_canonicalize("non/exist/path.txt");
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.ends_with("non/exist/path.txt"));
}
}
});
let _ = env::set_current_dir(original_cwd);
if let Err(e) = test_result {
std::panic::resume_unwind(e);
}
}
#[test]
fn test_resolve_mixed_existing_nonexisting() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let existing_dir = base_path.join("existing_dir");
fs::create_dir(&existing_dir).unwrap();
let existing_file = existing_dir.join("existing_file.txt");
fs::write(&existing_file, "test").unwrap();
let mixed_path = existing_file.join("non/existing/path.txt");
let result = soft_canonicalize(mixed_path);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains("existing_file.txt"));
#[cfg(windows)]
assert!(resolved
.to_string_lossy()
.contains("non\\existing\\path.txt"));
#[cfg(not(windows))]
assert!(resolved.to_string_lossy().contains("non/existing/path.txt"));
let partial_path = existing_dir.join("non_existing/file.txt");
let result = soft_canonicalize(partial_path);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains("existing_dir"));
#[cfg(windows)]
assert!(resolved
.to_string_lossy()
.contains("non_existing\\file.txt"));
#[cfg(not(windows))]
assert!(resolved.to_string_lossy().contains("non_existing/file.txt"));
}
#[cfg(unix)]
#[test]
fn test_resolve_dot_symlinks() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let link_dot = base_path.join("link_to_dot");
symlink(".", &link_dot).unwrap();
let test_path = link_dot.join("some/nonexisting/path.txt");
let result = soft_canonicalize(test_path);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
let expected_suffix = base_path.join("some/nonexisting/path.txt");
let resolved_str = resolved.to_string_lossy();
let expected_str = expected_suffix.to_string_lossy();
assert!(
resolved_str == expected_str || resolved_str == format!("/private{expected_str}"),
"Mismatch:\n left: {resolved_str}\n right: {expected_str} or /private{expected_str}"
);
}
#[test]
fn test_resolve_parent_traversal_mixed() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let level1 = base_path.join("level1");
let level2 = level1.join("level2");
fs::create_dir_all(&level2).unwrap();
let basic_traversal = level2
.join("subdir")
.join("..")
.join("..")
.join("..")
.join("target.txt");
let result = soft_canonicalize(basic_traversal);
assert!(result.is_ok());
let resolved = result.unwrap();
let expected = fs::canonicalize(base_path).unwrap().join("target.txt");
#[cfg(not(feature = "dunce"))]
{
assert_eq!(resolved, expected, "Without dunce: exact match");
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let resolved_str = resolved.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(!resolved_str.starts_with(r"\\?\"), "dunce should simplify");
assert!(expected_str.starts_with(r"\\?\"), "std returns UNC");
}
#[cfg(not(windows))]
{
assert_eq!(resolved, expected);
}
}
let dir_a = base_path.join("dirA");
let dir_b = base_path.join("dirB");
fs::create_dir(&dir_a).unwrap();
fs::create_dir(dir_b).unwrap();
let traversal_path = dir_a.join("../dirB/non_existing_file.txt");
let result = soft_canonicalize(traversal_path);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains("dirB"));
assert!(resolved.to_string_lossy().contains("non_existing_file.txt"));
assert!(!resolved.to_string_lossy().contains("dirA"));
assert!(!resolved.to_string_lossy().contains(".."));
let complex_traversal = dir_a
.join("../../../temp/../")
.join(base_path.file_name().unwrap())
.join("dirB/deep/non/existing.txt");
let result = soft_canonicalize(complex_traversal);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(!resolved.to_string_lossy().contains(".."));
}
#[cfg(unix)]
#[test]
fn test_resolve_symlink_loops_with_suffix() {
use std::os::unix::fs::symlink;
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let link_x = base_path.join("linkX");
symlink("linkX/inside", &link_x).unwrap();
let loop_path = link_x.join("foo/bar.txt");
let result = soft_canonicalize(loop_path);
assert!(result.is_ok() || result.is_err());
if let Ok(resolved) = result {
assert!(resolved.is_absolute());
}
}
#[test]
fn test_resolve_through_file() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let file_path = base_path.join("regular_file.txt");
fs::write(&file_path, "test content").unwrap();
let through_file = file_path.join("subdir/file.txt");
let result = soft_canonicalize(through_file);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains("regular_file.txt"));
#[cfg(windows)]
assert!(resolved.to_string_lossy().contains("subdir\\file.txt"));
#[cfg(not(windows))]
assert!(resolved.to_string_lossy().contains("subdir/file.txt"));
}
#[test]
fn test_resolve_unusual_characters() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let unusual_paths = vec![
"path with spaces/file.txt",
"path-with-dashes/file.txt",
"path_with_underscores/file.txt",
"pathwith䏿–‡/file.txt",
"path.with.dots/file.txt",
];
for unusual_path in unusual_paths {
let full_path = base_path.join(unusual_path);
let result = soft_canonicalize(&full_path);
assert!(result.is_ok(), "Failed to resolve path: {unusual_path}");
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains("file.txt"));
}
}
#[test]
fn test_resolve_deep_paths() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let basic_deep_path = base_path.join("a/b/c/d/e/file.txt");
let result = soft_canonicalize(&basic_deep_path);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
let expected = fs::canonicalize(base_path)
.unwrap()
.join("a/b/c/d/e/file.txt");
#[cfg(not(feature = "dunce"))]
{
assert_eq!(resolved, expected, "Without dunce: exact match");
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let resolved_str = resolved.to_string_lossy();
let expected_str = expected.to_string_lossy();
assert!(!resolved_str.starts_with(r"\\?\"), "dunce should simplify");
assert!(expected_str.starts_with(r"\\?\"), "std returns UNC");
}
#[cfg(not(windows))]
{
assert_eq!(resolved, expected);
}
}
let mut current = base_path.to_path_buf();
for i in 0..10 {
current = current.join(format!("level_{i}"));
fs::create_dir(¤t).unwrap();
}
let mut deep_path = current;
for i in 0..50 {
deep_path = deep_path.join(format!("deep_{i}"));
}
deep_path = deep_path.join("final_file.txt");
let result = soft_canonicalize(&deep_path);
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains("level_9")); assert!(resolved.to_string_lossy().contains("deep_49")); assert!(resolved.to_string_lossy().contains("final_file.txt"));
}
#[test]
fn test_resolve_from_different_cwd() {
let _lock = WORKING_DIR_MUTEX.lock().unwrap();
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let sub_dir1 = base_path.join("sub1");
let sub_dir2 = base_path.join("sub2");
fs::create_dir(&sub_dir1).unwrap();
fs::create_dir(&sub_dir2).unwrap();
let original_cwd = env::current_dir().unwrap();
let test_result = std::panic::catch_unwind(|| {
if sub_dir1.exists() {
env::set_current_dir(&sub_dir1).unwrap();
}
let result1 = soft_canonicalize("../sub2/non_existing.txt");
assert!(result1.is_ok());
let resolved1 = result1.unwrap();
assert!(resolved1.is_absolute());
assert!(resolved1.to_string_lossy().contains("sub2"));
assert!(resolved1.to_string_lossy().contains("non_existing.txt"));
if sub_dir2.exists() {
env::set_current_dir(&sub_dir2).unwrap();
}
let result2 = soft_canonicalize("../sub1/non_existing.txt");
assert!(result2.is_ok());
let resolved2 = result2.unwrap();
assert!(resolved2.is_absolute());
assert!(resolved2.to_string_lossy().contains("sub1"));
});
let _ = env::set_current_dir(original_cwd);
if let Err(e) = test_result {
std::panic::resume_unwind(e);
}
}
#[test]
fn test_resolve_minimal_paths() {
let _lock = WORKING_DIR_MUTEX.lock().unwrap();
use std::env;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let original_cwd = env::current_dir().unwrap();
let test_result = std::panic::catch_unwind(|| {
if temp_dir.path().exists() {
if let Ok(()) = env::set_current_dir(temp_dir.path()) {
let result = soft_canonicalize(".");
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
let result = soft_canonicalize("..");
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
let result = soft_canonicalize("single_file.txt");
assert!(result.is_ok());
let resolved = result.unwrap();
assert!(resolved.is_absolute());
assert!(resolved.to_string_lossy().contains("single_file.txt"));
}
}
});
let _ = env::set_current_dir(original_cwd);
if let Err(e) = test_result {
std::panic::resume_unwind(e);
}
}
#[test]
fn test_std_compatibility_existing_paths() {
let temp_dir = TempDir::new().unwrap();
let base_path = temp_dir.path();
let existing_file = base_path.join("existing.txt");
let existing_dir = base_path.join("existing_dir");
fs::write(&existing_file, "test").unwrap();
fs::create_dir(&existing_dir).unwrap();
for existing_path in [&existing_file, &existing_dir, base_path] {
let std_result = fs::canonicalize(existing_path);
let our_result = soft_canonicalize(existing_path);
assert!(
std_result.is_ok(),
"std::fs::canonicalize should work for existing path"
);
assert!(
our_result.is_ok(),
"soft_canonicalize should work for existing path"
);
let std_canonical = std_result.unwrap();
let our_canonical = our_result.unwrap();
assert!(std_canonical.is_absolute());
assert!(our_canonical.is_absolute());
let str_literal = existing_path.to_string_lossy();
let our_str_result = soft_canonicalize(str_literal.as_ref()).unwrap();
let std_str_result = fs::canonicalize(str_literal.as_ref()).unwrap();
#[cfg(not(feature = "dunce"))]
{
assert_eq!(our_str_result, std_str_result, "Without dunce: exact match");
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let our_str = our_str_result.to_string_lossy();
let std_str = std_str_result.to_string_lossy();
assert!(!our_str.starts_with(r"\\?\"), "dunce should simplify");
assert!(std_str.starts_with(r"\\?\"), "std returns UNC");
}
#[cfg(not(windows))]
{
assert_eq!(our_str_result, std_str_result);
}
}
let pathbuf = existing_path.to_path_buf();
let our_pathbuf_result = soft_canonicalize(pathbuf.clone()).unwrap();
let std_pathbuf_result = fs::canonicalize(pathbuf).unwrap();
#[cfg(not(feature = "dunce"))]
{
assert_eq!(
our_pathbuf_result, std_pathbuf_result,
"Without dunce: exact match"
);
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let our_buf_str = our_pathbuf_result.to_string_lossy();
let std_buf_str = std_pathbuf_result.to_string_lossy();
assert!(!our_buf_str.starts_with(r"\\?\"), "dunce should simplify");
assert!(std_buf_str.starts_with(r"\\?\"), "std returns UNC");
}
#[cfg(not(windows))]
{
assert_eq!(our_pathbuf_result, std_pathbuf_result);
}
}
let pathbuf = existing_path.to_path_buf();
let our_ref_result = soft_canonicalize(&pathbuf).unwrap();
let std_ref_result = fs::canonicalize(&pathbuf).unwrap();
#[cfg(not(feature = "dunce"))]
{
assert_eq!(our_ref_result, std_ref_result, "Without dunce: exact match");
}
#[cfg(feature = "dunce")]
{
#[cfg(windows)]
{
let our_ref_str = our_ref_result.to_string_lossy();
let std_ref_str = std_ref_result.to_string_lossy();
assert!(!our_ref_str.starts_with(r"\\?\"), "dunce should simplify");
assert!(std_ref_str.starts_with(r"\\?\"), "std returns UNC");
}
#[cfg(not(windows))]
{
assert_eq!(our_ref_result, std_ref_result);
}
}
}
}
}