Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions crates/pet-homebrew/src/environment_locations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,43 @@ mod tests {
.iter()
.any(|path| path == &missing_homebrew_prefix.join("bin")));
}

#[test]
fn homebrew_prefix_bin_returns_results_without_env_var() {
let env_vars = EnvVariables {
home: None,
root: None,
path: None,
homebrew_prefix: None,
known_global_search_locations: vec![],
};

// Should not panic and should return whatever standard paths exist
let prefix_bins = get_homebrew_prefix_bin(&env_vars);
// All returned paths should actually exist
for path in &prefix_bins {
assert!(path.exists(), "{:?} should exist", path);
}
}

#[test]
fn homebrew_prefix_bin_does_not_duplicate_when_env_var_matches_existing_dir() {
// Create a temp dir to act as a custom homebrew prefix.
// Call get_homebrew_prefix_bin twice with the same prefix to ensure
// the env var path only appears once in the result.
let custom_prefix = tempdir().unwrap();
let custom_bin = custom_prefix.path().join("bin");
fs::create_dir_all(&custom_bin).unwrap();
let env_vars = EnvVariables {
home: None,
root: None,
path: None,
homebrew_prefix: Some(custom_prefix.path().to_string_lossy().to_string()),
known_global_search_locations: vec![],
};

let prefix_bins = get_homebrew_prefix_bin(&env_vars);
let count = prefix_bins.iter().filter(|p| **p == custom_bin).count();
assert_eq!(count, 1, "Custom bin path should appear exactly once");
}
}
101 changes: 101 additions & 0 deletions crates/pet-homebrew/src/environments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,4 +167,105 @@ mod tests {
Some("3.11.9".to_string())
);
}

#[test]
fn extract_version_from_opt_homebrew_path() {
assert_eq!(
get_version(&PathBuf::from(
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12"
)),
Some("3.12.3".to_string())
);
}

#[test]
fn extract_version_from_usr_local_cellar_path() {
assert_eq!(
get_version(&PathBuf::from(
"/usr/local/Cellar/python@3.8/3.8.20/Frameworks/Python.framework/Versions/3.8/bin/python3.8"
)),
Some("3.8.20".to_string())
);
}

#[test]
fn extract_version_returns_none_for_path_without_version() {
assert_eq!(get_version(&PathBuf::from("/usr/bin/python3")), None);
}

#[test]
fn get_prefix_always_returns_none() {
assert!(get_prefix(&PathBuf::from(
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12"
))
.is_none());
assert!(get_prefix(&PathBuf::from(
"/home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.4/bin/python3.12"
))
.is_none());
assert!(get_prefix(&PathBuf::from(
"/usr/local/Cellar/python@3.8/3.8.20/bin/python3.8"
))
.is_none());
}

#[test]
fn get_python_info_returns_correct_kind_and_executable() {
let bin_exe = PathBuf::from("/home/linuxbrew/.linuxbrew/bin/python3.12");
let resolved_exe =
PathBuf::from("/home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.4/bin/python3.12");

let env = get_python_info(&bin_exe, &resolved_exe).unwrap();

assert_eq!(env.kind, Some(PythonEnvironmentKind::Homebrew));
assert_eq!(env.executable, Some(bin_exe.clone()));
assert_eq!(env.version, Some("3.12.4".to_string()));
assert_eq!(env.prefix, None);
// Both bin exe and resolved exe should be in symlinks
let symlinks = env.symlinks.unwrap();
assert!(symlinks.contains(&bin_exe));
assert!(symlinks.contains(&resolved_exe));
}

#[test]
fn get_python_info_returns_none_version_for_unversioned_path() {
let bin_exe = PathBuf::from("/home/linuxbrew/.linuxbrew/bin/python3");
let resolved_exe = PathBuf::from("/home/linuxbrew/.linuxbrew/bin/python3");

let env = get_python_info(&bin_exe, &resolved_exe).unwrap();

assert_eq!(env.kind, Some(PythonEnvironmentKind::Homebrew));
assert_eq!(env.version, None);
}

#[test]
fn get_python_info_for_opt_homebrew_path() {
let bin_exe = PathBuf::from("/opt/homebrew/bin/python3.12");
let resolved_exe = PathBuf::from(
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
);

let env = get_python_info(&bin_exe, &resolved_exe).unwrap();

assert_eq!(env.kind, Some(PythonEnvironmentKind::Homebrew));
assert_eq!(env.executable, Some(bin_exe.clone()));
assert_eq!(env.version, Some("3.12.3".to_string()));
let symlinks = env.symlinks.unwrap();
assert!(symlinks.contains(&bin_exe));
assert!(symlinks.contains(&resolved_exe));
}

#[test]
fn get_python_info_for_usr_local_cellar_path() {
let bin_exe = PathBuf::from("/usr/local/bin/python3.8");
let resolved_exe = PathBuf::from(
"/usr/local/Cellar/python@3.8/3.8.20/Frameworks/Python.framework/Versions/3.8/bin/python3.8",
);

let env = get_python_info(&bin_exe, &resolved_exe).unwrap();

assert_eq!(env.kind, Some(PythonEnvironmentKind::Homebrew));
assert_eq!(env.executable, Some(bin_exe));
assert_eq!(env.version, Some("3.8.20".to_string()));
}
}
96 changes: 96 additions & 0 deletions crates/pet-homebrew/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -281,4 +281,100 @@ mod tests {
);
assert!(locator.try_from(&conda).is_none());
}

#[test]
fn try_from_identifies_opt_homebrew_python() {
let locator = Homebrew::from(&TestEnvironment {
homebrew_prefix: None,
});
let env = PythonEnv::new(
PathBuf::from(
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
),
None,
None,
);

let homebrew_env = locator.try_from(&env).unwrap();

assert_eq!(homebrew_env.kind, Some(PythonEnvironmentKind::Homebrew));
assert_eq!(
homebrew_env.executable,
Some(PathBuf::from("/opt/homebrew/bin/python3.12"))
);
assert_eq!(homebrew_env.version, Some("3.12.3".to_string()));
}

#[test]
fn try_from_identifies_usr_local_cellar_python() {
let locator = Homebrew::from(&TestEnvironment {
homebrew_prefix: None,
});
let env = PythonEnv::new(
PathBuf::from(
"/usr/local/Cellar/python@3.8/3.8.20/Frameworks/Python.framework/Versions/3.8/bin/python3.8",
),
None,
None,
);

let homebrew_env = locator.try_from(&env).unwrap();

assert_eq!(homebrew_env.kind, Some(PythonEnvironmentKind::Homebrew));
assert_eq!(
homebrew_env.executable,
Some(PathBuf::from("/usr/local/bin/python3.8"))
);
assert_eq!(homebrew_env.version, Some("3.8.20".to_string()));
}

#[test]
fn try_from_rejects_conda_env_when_parent_is_conda() {
let locator = Homebrew::from(&TestEnvironment {
homebrew_prefix: None,
});
// Create a directory that looks like a conda env (has conda-meta)
let conda_root = tempdir().unwrap();
fs::create_dir_all(conda_root.path().join("conda-meta")).unwrap();
// Place executable directly in the conda-meta parent directory
let exe = conda_root.path().join("python3.12");
fs::write(&exe, b"").unwrap();

let env = PythonEnv::new(exe, None, None);
assert!(locator.try_from(&env).is_none());
}

#[test]
fn try_from_rejects_conda_env_when_grandparent_is_conda() {
let locator = Homebrew::from(&TestEnvironment {
homebrew_prefix: None,
});
// Create a directory that looks like a conda env (has conda-meta)
let conda_root = tempdir().unwrap();
fs::create_dir_all(conda_root.path().join("conda-meta")).unwrap();
let bin_dir = conda_root.path().join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let exe = bin_dir.join("python3.12");
fs::write(&exe, b"").unwrap();

let env = PythonEnv::new(exe, None, None);
assert!(locator.try_from(&env).is_none());
}

#[test]
fn try_from_rejects_conda_env_via_prefix() {
let locator = Homebrew::from(&TestEnvironment {
homebrew_prefix: None,
});
// Conda env detected via prefix having conda-meta
let conda_root = tempdir().unwrap();
fs::create_dir_all(conda_root.path().join("conda-meta")).unwrap();
let bin_dir = conda_root.path().join("bin");
fs::create_dir_all(&bin_dir).unwrap();
let exe = bin_dir.join("python3.12");
fs::write(&exe, b"").unwrap();

let env = PythonEnv::new(exe, Some(conda_root.path().to_path_buf()), None);
assert!(locator.try_from(&env).is_none());
}
}
70 changes: 69 additions & 1 deletion crates/pet-homebrew/src/sym_links.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ pub fn get_known_symlinks_impl(
}
}

#[cfg(test)]
#[cfg(all(test, unix))]
mod tests {
use super::*;

Expand All @@ -262,6 +262,29 @@ mod tests {
assert!(!is_homebrew_python(Path::new("/usr/bin/python3.12")));
}

#[test]
fn is_homebrew_python_recognizes_opt_homebrew_bin_paths() {
assert!(is_homebrew_python(Path::new(
"/opt/homebrew/bin/python3.12"
)));
assert!(is_homebrew_python(Path::new(
"/opt/homebrew/opt/python@3.12/bin/python3.12"
)));
assert!(is_homebrew_python(Path::new(
"/opt/homebrew/Frameworks/Python.framework/Versions/3.12/bin/python3.12"
)));
}

#[test]
fn is_homebrew_python_rejects_non_homebrew_paths() {
assert!(!is_homebrew_python(Path::new("/usr/local/bin/python3.12")));
assert!(!is_homebrew_python(Path::new("/usr/bin/python3")));
assert!(!is_homebrew_python(Path::new(
"/home/user/.pyenv/versions/3.12.0/bin/python3.12"
)));
assert!(!is_homebrew_python(Path::new("")));
}

#[test]
fn known_symlink_templates_include_resolved_executable_for_linuxbrew() {
let resolved_exe =
Expand All @@ -278,4 +301,49 @@ mod tests {
.is_empty()
);
}

#[test]
fn known_symlink_templates_include_self_for_opt_homebrew() {
let resolved_exe = PathBuf::from(
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
);
let symlinks = get_known_symlinks_impl(&resolved_exe, &"3.12.3".to_string());

assert!(symlinks.contains(&resolved_exe));
assert!(symlinks.len() >= 1);
}

#[test]
fn known_symlink_templates_include_self_for_usr_local_cellar() {
let resolved_exe = PathBuf::from(
"/usr/local/Cellar/python@3.8/3.8.20/Frameworks/Python.framework/Versions/3.8/bin/python3.8",
);
let symlinks = get_known_symlinks_impl(&resolved_exe, &"3.8.20".to_string());

assert!(symlinks.contains(&resolved_exe));
assert!(symlinks.len() >= 1);
}

#[test]
fn known_symlink_templates_return_empty_when_version_regex_does_not_match() {
// Path under /opt/homebrew but without a python@version segment
let resolved_exe = PathBuf::from("/opt/homebrew/bin/python3.12");
let symlinks = get_known_symlinks_impl(&resolved_exe, &"3.12.0".to_string());

// No python@version/ in path, so regex won't capture → returns empty
assert!(symlinks.is_empty());
}

#[test]
fn known_symlink_templates_for_linuxbrew_contain_expected_paths() {
let resolved_exe =
PathBuf::from("/home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.4/bin/python3.12");
let symlinks = get_known_symlinks_impl(&resolved_exe, &"3.12.4".to_string());

// The resolved exe itself is always included
assert!(symlinks.contains(&resolved_exe));
// On a test system without real symlinks, only the resolved exe will pass validation.
// But verify the function doesn't panic and returns at least the resolved exe.
assert!(!symlinks.is_empty());
}
}
Loading