diff --git a/Cargo.lock b/Cargo.lock index 8c49f670..a4665a30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -690,6 +690,7 @@ dependencies = [ "regex", "serde", "serde_json", + "tempfile", ] [[package]] diff --git a/crates/pet-pyenv/Cargo.toml b/crates/pet-pyenv/Cargo.toml index 404ee53d..f44ec5d3 100644 --- a/crates/pet-pyenv/Cargo.toml +++ b/crates/pet-pyenv/Cargo.toml @@ -18,3 +18,6 @@ pet-fs = { path = "../pet-fs" } pet-conda = { path = "../pet-conda" } log = "0.4.21" regex = "1.10.4" + +[dev-dependencies] +tempfile = "3.10.1" diff --git a/crates/pet-pyenv/src/environment_locations.rs b/crates/pet-pyenv/src/environment_locations.rs index c395b2ae..4741395e 100644 --- a/crates/pet-pyenv/src/environment_locations.rs +++ b/crates/pet-pyenv/src/environment_locations.rs @@ -45,3 +45,108 @@ pub fn get_pyenv_dir(env_vars: &EnvVariables) -> Option { None => env_vars.pyenv.as_ref().map(PathBuf::from), } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use tempfile::tempdir; + + fn make_env_vars( + home: Option, + pyenv_root: Option, + pyenv: Option, + known_paths: Vec, + ) -> EnvVariables { + EnvVariables { + home, + root: None, + path: None, + pyenv_root, + pyenv, + known_global_search_locations: known_paths, + } + } + + // get_pyenv_dir tests + #[test] + fn get_pyenv_dir_prefers_pyenv_root_over_pyenv() { + let env = make_env_vars( + None, + Some("/custom/pyenv-root".to_string()), + Some("/other/pyenv".to_string()), + vec![], + ); + assert_eq!( + get_pyenv_dir(&env), + Some(PathBuf::from("/custom/pyenv-root")) + ); + } + + #[test] + fn get_pyenv_dir_falls_back_to_pyenv_env_var() { + let env = make_env_vars(None, None, Some("/fallback/pyenv".to_string()), vec![]); + assert_eq!(get_pyenv_dir(&env), Some(PathBuf::from("/fallback/pyenv"))); + } + + #[test] + fn get_pyenv_dir_returns_none_when_no_env_vars() { + let env = make_env_vars(None, None, None, vec![]); + assert_eq!(get_pyenv_dir(&env), None); + } + + // get_home_pyenv_dir tests + #[test] + fn get_home_pyenv_dir_returns_none_without_home() { + let env = make_env_vars(None, None, None, vec![]); + assert_eq!(get_home_pyenv_dir(&env), None); + } + + #[test] + fn get_home_pyenv_dir_returns_expected_path_with_home() { + let home = tempdir().unwrap(); + let env = make_env_vars(Some(home.path().to_path_buf()), None, None, vec![]); + let result = get_home_pyenv_dir(&env).unwrap(); + let path_str = result.to_string_lossy(); + if cfg!(windows) { + assert!( + path_str.contains(".pyenv"), + "Expected .pyenv in path: {}", + path_str + ); + assert!( + path_str.contains("pyenv-win"), + "Expected pyenv-win in path: {}", + path_str + ); + } else { + assert!(result.ends_with(".pyenv")); + } + } + + // get_binary_from_known_paths tests + #[test] + fn get_binary_from_known_paths_finds_pyenv_binary() { + let dir = tempdir().unwrap(); + let bin_name = if cfg!(windows) { "pyenv.bat" } else { "pyenv" }; + let exe = dir.path().join(bin_name); + fs::write(&exe, b"").unwrap(); + + let env = make_env_vars(None, None, None, vec![dir.path().to_path_buf()]); + let result = get_binary_from_known_paths(&env); + assert!(result.is_some()); + } + + #[test] + fn get_binary_from_known_paths_returns_none_when_not_found() { + let dir = tempdir().unwrap(); + let env = make_env_vars(None, None, None, vec![dir.path().to_path_buf()]); + assert!(get_binary_from_known_paths(&env).is_none()); + } + + #[test] + fn get_binary_from_known_paths_returns_none_for_empty_paths() { + let env = make_env_vars(None, None, None, vec![]); + assert!(get_binary_from_known_paths(&env).is_none()); + } +} diff --git a/crates/pet-pyenv/src/environments.rs b/crates/pet-pyenv/src/environments.rs index 0f2fb51d..d20d1829 100644 --- a/crates/pet-pyenv/src/environments.rs +++ b/crates/pet-pyenv/src/environments.rs @@ -100,3 +100,244 @@ fn get_version(folder_name: &str) -> Option { } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + use std::path::PathBuf; + use tempfile::tempdir; + + // get_version tests + #[test] + fn get_version_parses_stable_version() { + assert_eq!(get_version("3.10.10"), Some("3.10.10".to_string())); + assert_eq!(get_version("3.12.0"), Some("3.12.0".to_string())); + assert_eq!(get_version("2.7.18"), Some("2.7.18".to_string())); + } + + #[test] + fn get_version_parses_dev_version() { + assert_eq!(get_version("3.10-dev"), Some("3.10-dev".to_string())); + assert_eq!(get_version("3.13-dev"), Some("3.13-dev".to_string())); + } + + #[test] + fn get_version_parses_alpha_rc_version() { + assert_eq!(get_version("3.10.0a3"), Some("3.10.0a3".to_string())); + assert_eq!(get_version("3.12.0b1"), Some("3.12.0b1".to_string())); + } + + #[test] + fn get_version_returns_none_for_multi_letter_prerelease() { + // Known limitation: BETA_PYTHON_VERSION regex uses \w (single char) so multi-letter + // pre-release tags like "rc" are not captured. Real pyenv installs can have rc versions + // (e.g. 3.13.0rc1), but version detection falls back to header files in that case. + assert_eq!(get_version("3.11.0rc2"), None); + } + + #[test] + fn get_version_parses_win32_version() { + assert_eq!(get_version("3.11.0a4-win32"), Some("3.11.0a4".to_string())); + } + + #[test] + fn get_version_returns_none_for_non_version_strings() { + assert_eq!(get_version("mambaforge-4.10.1-4"), None); + assert_eq!(get_version("pypy3.9-7.3.15"), None); + assert_eq!(get_version("my-virtual-env"), None); + assert_eq!(get_version(""), None); + } + + #[test] + fn get_version_returns_none_for_partial_version() { + assert_eq!(get_version("3.10"), None); + } + + // get_generic_python_environment tests + #[test] + fn get_generic_python_environment_with_stable_version_folder() { + let root = tempdir().unwrap(); + let env_path = root.path().join("3.12.0"); + let bin_dir = if cfg!(windows) { + env_path.join("Scripts") + } else { + env_path.join("bin") + }; + fs::create_dir_all(&bin_dir).unwrap(); + let exe = if cfg!(windows) { + bin_dir.join("python.exe") + } else { + bin_dir.join("python") + }; + fs::write(&exe, b"").unwrap(); + + let result = get_generic_python_environment(&exe, &env_path, &None).unwrap(); + + assert_eq!(result.kind, Some(PythonEnvironmentKind::Pyenv)); + assert_eq!( + result.executable.as_ref().unwrap().file_name(), + exe.file_name() + ); + assert_eq!(result.version, Some("3.12.0".to_string())); + assert_eq!( + result.prefix.as_ref().unwrap().file_name(), + env_path.file_name() + ); + assert!(result.manager.is_none()); + } + + #[test] + fn get_generic_python_environment_with_win32_folder_sets_x86_arch() { + let root = tempdir().unwrap(); + let env_path = root.path().join("3.11.0a4-win32"); + let bin_dir = if cfg!(windows) { + env_path.join("Scripts") + } else { + env_path.join("bin") + }; + fs::create_dir_all(&bin_dir).unwrap(); + let exe = if cfg!(windows) { + bin_dir.join("python.exe") + } else { + bin_dir.join("python") + }; + fs::write(&exe, b"").unwrap(); + + let result = get_generic_python_environment(&exe, &env_path, &None).unwrap(); + + assert_eq!(result.arch, Some(Architecture::X86)); + } + + #[test] + fn get_generic_python_environment_with_non_win32_folder_has_no_arch() { + let root = tempdir().unwrap(); + let env_path = root.path().join("3.12.0"); + let bin_dir = if cfg!(windows) { + env_path.join("Scripts") + } else { + env_path.join("bin") + }; + fs::create_dir_all(&bin_dir).unwrap(); + let exe = if cfg!(windows) { + bin_dir.join("python.exe") + } else { + bin_dir.join("python") + }; + fs::write(&exe, b"").unwrap(); + + let result = get_generic_python_environment(&exe, &env_path, &None).unwrap(); + + assert!(result.arch.is_none()); + } + + #[test] + fn get_generic_python_environment_includes_manager_when_provided() { + let root = tempdir().unwrap(); + let env_path = root.path().join("3.12.0"); + let bin_dir = if cfg!(windows) { + env_path.join("Scripts") + } else { + env_path.join("bin") + }; + fs::create_dir_all(&bin_dir).unwrap(); + let exe = if cfg!(windows) { + bin_dir.join("python.exe") + } else { + bin_dir.join("python") + }; + fs::write(&exe, b"").unwrap(); + + let mgr = EnvManager::new( + PathBuf::from("/usr/bin/pyenv"), + pet_core::manager::EnvManagerType::Pyenv, + Some("2.4.0".to_string()), + ); + let result = get_generic_python_environment(&exe, &env_path, &Some(mgr.clone())).unwrap(); + + assert_eq!(result.manager, Some(mgr)); + } + + #[test] + fn get_generic_python_environment_with_unrecognized_folder_name() { + let root = tempdir().unwrap(); + let env_path = root.path().join("mambaforge-4.10.1-4"); + let bin_dir = if cfg!(windows) { + env_path.join("Scripts") + } else { + env_path.join("bin") + }; + fs::create_dir_all(&bin_dir).unwrap(); + let exe = if cfg!(windows) { + bin_dir.join("python.exe") + } else { + bin_dir.join("python") + }; + fs::write(&exe, b"").unwrap(); + + let result = get_generic_python_environment(&exe, &env_path, &None).unwrap(); + + assert_eq!(result.kind, Some(PythonEnvironmentKind::Pyenv)); + // No version extractable from folder name and no header files + assert!(result.version.is_none()); + } + + // get_virtual_env_environment tests + #[test] + fn get_virtual_env_returns_none_without_pyvenv_cfg() { + let root = tempdir().unwrap(); + let env_path = root.path().join("my-venv"); + let bin_dir = if cfg!(windows) { + env_path.join("Scripts") + } else { + env_path.join("bin") + }; + fs::create_dir_all(&bin_dir).unwrap(); + let exe = if cfg!(windows) { + bin_dir.join("python.exe") + } else { + bin_dir.join("python") + }; + fs::write(&exe, b"").unwrap(); + + let result = get_virtual_env_environment(&exe, &env_path, &None); + + assert!(result.is_none()); + } + + #[test] + fn get_virtual_env_returns_env_with_pyvenv_cfg() { + let root = tempdir().unwrap(); + let env_path = root.path().join("my-venv"); + let bin_dir = if cfg!(windows) { + env_path.join("Scripts") + } else { + env_path.join("bin") + }; + fs::create_dir_all(&bin_dir).unwrap(); + let exe = if cfg!(windows) { + bin_dir.join("python.exe") + } else { + bin_dir.join("python") + }; + fs::write(&exe, b"").unwrap(); + fs::write( + env_path.join("pyvenv.cfg"), + "version = 3.12.0\nhome = /usr/bin\n", + ) + .unwrap(); + + let result = get_virtual_env_environment(&exe, &env_path, &None).unwrap(); + + assert_eq!(result.kind, Some(PythonEnvironmentKind::PyenvVirtualEnv)); + assert_eq!(result.version, Some("3.12.0".to_string())); + assert_eq!( + result.executable.as_ref().unwrap().file_name(), + exe.file_name() + ); + assert_eq!( + result.prefix.as_ref().unwrap().file_name(), + env_path.file_name() + ); + } +}