Skip to content

Commit 98df781

Browse files
committed
test: add unit tests for pet-pyenv crate (Fixes #389)
1 parent 6072b60 commit 98df781

4 files changed

Lines changed: 338 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/pet-pyenv/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,6 @@ pet-fs = { path = "../pet-fs" }
1818
pet-conda = { path = "../pet-conda" }
1919
log = "0.4.21"
2020
regex = "1.10.4"
21+
22+
[dev-dependencies]
23+
tempfile = "3.10.1"

crates/pet-pyenv/src/environment_locations.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,108 @@ pub fn get_pyenv_dir(env_vars: &EnvVariables) -> Option<PathBuf> {
4545
None => env_vars.pyenv.as_ref().map(PathBuf::from),
4646
}
4747
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use super::*;
52+
use std::fs;
53+
use tempfile::tempdir;
54+
55+
fn make_env_vars(
56+
home: Option<PathBuf>,
57+
pyenv_root: Option<String>,
58+
pyenv: Option<String>,
59+
known_paths: Vec<PathBuf>,
60+
) -> EnvVariables {
61+
EnvVariables {
62+
home,
63+
root: None,
64+
path: None,
65+
pyenv_root,
66+
pyenv,
67+
known_global_search_locations: known_paths,
68+
}
69+
}
70+
71+
// get_pyenv_dir tests
72+
#[test]
73+
fn get_pyenv_dir_prefers_pyenv_root_over_pyenv() {
74+
let env = make_env_vars(
75+
None,
76+
Some("/custom/pyenv-root".to_string()),
77+
Some("/other/pyenv".to_string()),
78+
vec![],
79+
);
80+
assert_eq!(
81+
get_pyenv_dir(&env),
82+
Some(PathBuf::from("/custom/pyenv-root"))
83+
);
84+
}
85+
86+
#[test]
87+
fn get_pyenv_dir_falls_back_to_pyenv_env_var() {
88+
let env = make_env_vars(None, None, Some("/fallback/pyenv".to_string()), vec![]);
89+
assert_eq!(get_pyenv_dir(&env), Some(PathBuf::from("/fallback/pyenv")));
90+
}
91+
92+
#[test]
93+
fn get_pyenv_dir_returns_none_when_no_env_vars() {
94+
let env = make_env_vars(None, None, None, vec![]);
95+
assert_eq!(get_pyenv_dir(&env), None);
96+
}
97+
98+
// get_home_pyenv_dir tests
99+
#[test]
100+
fn get_home_pyenv_dir_returns_none_without_home() {
101+
let env = make_env_vars(None, None, None, vec![]);
102+
assert_eq!(get_home_pyenv_dir(&env), None);
103+
}
104+
105+
#[test]
106+
fn get_home_pyenv_dir_returns_expected_path_with_home() {
107+
let home = tempdir().unwrap();
108+
let env = make_env_vars(Some(home.path().to_path_buf()), None, None, vec![]);
109+
let result = get_home_pyenv_dir(&env).unwrap();
110+
let path_str = result.to_string_lossy();
111+
if cfg!(windows) {
112+
assert!(
113+
path_str.contains(".pyenv"),
114+
"Expected .pyenv in path: {}",
115+
path_str
116+
);
117+
assert!(
118+
path_str.contains("pyenv-win"),
119+
"Expected pyenv-win in path: {}",
120+
path_str
121+
);
122+
} else {
123+
assert!(result.ends_with(".pyenv"));
124+
}
125+
}
126+
127+
// get_binary_from_known_paths tests
128+
#[test]
129+
fn get_binary_from_known_paths_finds_pyenv_binary() {
130+
let dir = tempdir().unwrap();
131+
let bin_name = if cfg!(windows) { "pyenv.bat" } else { "pyenv" };
132+
let exe = dir.path().join(bin_name);
133+
fs::write(&exe, b"").unwrap();
134+
135+
let env = make_env_vars(None, None, None, vec![dir.path().to_path_buf()]);
136+
let result = get_binary_from_known_paths(&env);
137+
assert!(result.is_some());
138+
}
139+
140+
#[test]
141+
fn get_binary_from_known_paths_returns_none_when_not_found() {
142+
let dir = tempdir().unwrap();
143+
let env = make_env_vars(None, None, None, vec![dir.path().to_path_buf()]);
144+
assert!(get_binary_from_known_paths(&env).is_none());
145+
}
146+
147+
#[test]
148+
fn get_binary_from_known_paths_returns_none_for_empty_paths() {
149+
let env = make_env_vars(None, None, None, vec![]);
150+
assert!(get_binary_from_known_paths(&env).is_none());
151+
}
152+
}

crates/pet-pyenv/src/environments.rs

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,232 @@ fn get_version(folder_name: &str) -> Option<String> {
100100
}
101101
}
102102
}
103+
104+
#[cfg(test)]
105+
mod tests {
106+
use super::*;
107+
use std::fs;
108+
use std::path::PathBuf;
109+
use tempfile::tempdir;
110+
111+
// get_version tests
112+
#[test]
113+
fn get_version_parses_stable_version() {
114+
assert_eq!(get_version("3.10.10"), Some("3.10.10".to_string()));
115+
assert_eq!(get_version("3.12.0"), Some("3.12.0".to_string()));
116+
assert_eq!(get_version("2.7.18"), Some("2.7.18".to_string()));
117+
}
118+
119+
#[test]
120+
fn get_version_parses_dev_version() {
121+
assert_eq!(get_version("3.10-dev"), Some("3.10-dev".to_string()));
122+
assert_eq!(get_version("3.13-dev"), Some("3.13-dev".to_string()));
123+
}
124+
125+
#[test]
126+
fn get_version_parses_alpha_rc_version() {
127+
assert_eq!(get_version("3.10.0a3"), Some("3.10.0a3".to_string()));
128+
assert_eq!(get_version("3.12.0b1"), Some("3.12.0b1".to_string()));
129+
}
130+
131+
#[test]
132+
fn get_version_returns_none_for_multi_letter_prerelease() {
133+
// Known limitation: BETA_PYTHON_VERSION regex uses \w (single char) so multi-letter
134+
// pre-release tags like "rc" are not captured. Real pyenv installs can have rc versions
135+
// (e.g. 3.13.0rc1), but version detection falls back to header files in that case.
136+
assert_eq!(get_version("3.11.0rc2"), None);
137+
}
138+
139+
#[test]
140+
fn get_version_parses_win32_version() {
141+
assert_eq!(get_version("3.11.0a4-win32"), Some("3.11.0a4".to_string()));
142+
}
143+
144+
#[test]
145+
fn get_version_returns_none_for_non_version_strings() {
146+
assert_eq!(get_version("mambaforge-4.10.1-4"), None);
147+
assert_eq!(get_version("pypy3.9-7.3.15"), None);
148+
assert_eq!(get_version("my-virtual-env"), None);
149+
assert_eq!(get_version(""), None);
150+
}
151+
152+
#[test]
153+
fn get_version_returns_none_for_partial_version() {
154+
assert_eq!(get_version("3.10"), None);
155+
}
156+
157+
// get_generic_python_environment tests
158+
#[test]
159+
fn get_generic_python_environment_with_stable_version_folder() {
160+
let root = tempdir().unwrap();
161+
let env_path = root.path().join("3.12.0");
162+
let bin_dir = if cfg!(windows) {
163+
env_path.join("Scripts")
164+
} else {
165+
env_path.join("bin")
166+
};
167+
fs::create_dir_all(&bin_dir).unwrap();
168+
let exe = if cfg!(windows) {
169+
bin_dir.join("python.exe")
170+
} else {
171+
bin_dir.join("python")
172+
};
173+
fs::write(&exe, b"").unwrap();
174+
175+
let result = get_generic_python_environment(&exe, &env_path, &None).unwrap();
176+
177+
assert_eq!(result.kind, Some(PythonEnvironmentKind::Pyenv));
178+
assert_eq!(result.executable, Some(exe));
179+
assert_eq!(result.version, Some("3.12.0".to_string()));
180+
assert_eq!(result.prefix, Some(env_path));
181+
assert!(result.manager.is_none());
182+
}
183+
184+
#[test]
185+
fn get_generic_python_environment_with_win32_folder_sets_x86_arch() {
186+
let root = tempdir().unwrap();
187+
let env_path = root.path().join("3.11.0a4-win32");
188+
let bin_dir = if cfg!(windows) {
189+
env_path.join("Scripts")
190+
} else {
191+
env_path.join("bin")
192+
};
193+
fs::create_dir_all(&bin_dir).unwrap();
194+
let exe = if cfg!(windows) {
195+
bin_dir.join("python.exe")
196+
} else {
197+
bin_dir.join("python")
198+
};
199+
fs::write(&exe, b"").unwrap();
200+
201+
let result = get_generic_python_environment(&exe, &env_path, &None).unwrap();
202+
203+
assert_eq!(result.arch, Some(Architecture::X86));
204+
}
205+
206+
#[test]
207+
fn get_generic_python_environment_with_non_win32_folder_has_no_arch() {
208+
let root = tempdir().unwrap();
209+
let env_path = root.path().join("3.12.0");
210+
let bin_dir = if cfg!(windows) {
211+
env_path.join("Scripts")
212+
} else {
213+
env_path.join("bin")
214+
};
215+
fs::create_dir_all(&bin_dir).unwrap();
216+
let exe = if cfg!(windows) {
217+
bin_dir.join("python.exe")
218+
} else {
219+
bin_dir.join("python")
220+
};
221+
fs::write(&exe, b"").unwrap();
222+
223+
let result = get_generic_python_environment(&exe, &env_path, &None).unwrap();
224+
225+
assert!(result.arch.is_none());
226+
}
227+
228+
#[test]
229+
fn get_generic_python_environment_includes_manager_when_provided() {
230+
let root = tempdir().unwrap();
231+
let env_path = root.path().join("3.12.0");
232+
let bin_dir = if cfg!(windows) {
233+
env_path.join("Scripts")
234+
} else {
235+
env_path.join("bin")
236+
};
237+
fs::create_dir_all(&bin_dir).unwrap();
238+
let exe = if cfg!(windows) {
239+
bin_dir.join("python.exe")
240+
} else {
241+
bin_dir.join("python")
242+
};
243+
fs::write(&exe, b"").unwrap();
244+
245+
let mgr = EnvManager::new(
246+
PathBuf::from("/usr/bin/pyenv"),
247+
pet_core::manager::EnvManagerType::Pyenv,
248+
Some("2.4.0".to_string()),
249+
);
250+
let result = get_generic_python_environment(&exe, &env_path, &Some(mgr.clone())).unwrap();
251+
252+
assert_eq!(result.manager, Some(mgr));
253+
}
254+
255+
#[test]
256+
fn get_generic_python_environment_with_unrecognized_folder_name() {
257+
let root = tempdir().unwrap();
258+
let env_path = root.path().join("mambaforge-4.10.1-4");
259+
let bin_dir = if cfg!(windows) {
260+
env_path.join("Scripts")
261+
} else {
262+
env_path.join("bin")
263+
};
264+
fs::create_dir_all(&bin_dir).unwrap();
265+
let exe = if cfg!(windows) {
266+
bin_dir.join("python.exe")
267+
} else {
268+
bin_dir.join("python")
269+
};
270+
fs::write(&exe, b"").unwrap();
271+
272+
let result = get_generic_python_environment(&exe, &env_path, &None).unwrap();
273+
274+
assert_eq!(result.kind, Some(PythonEnvironmentKind::Pyenv));
275+
// No version extractable from folder name and no header files
276+
assert!(result.version.is_none());
277+
}
278+
279+
// get_virtual_env_environment tests
280+
#[test]
281+
fn get_virtual_env_returns_none_without_pyvenv_cfg() {
282+
let root = tempdir().unwrap();
283+
let env_path = root.path().join("my-venv");
284+
let bin_dir = if cfg!(windows) {
285+
env_path.join("Scripts")
286+
} else {
287+
env_path.join("bin")
288+
};
289+
fs::create_dir_all(&bin_dir).unwrap();
290+
let exe = if cfg!(windows) {
291+
bin_dir.join("python.exe")
292+
} else {
293+
bin_dir.join("python")
294+
};
295+
fs::write(&exe, b"").unwrap();
296+
297+
let result = get_virtual_env_environment(&exe, &env_path, &None);
298+
299+
assert!(result.is_none());
300+
}
301+
302+
#[test]
303+
fn get_virtual_env_returns_env_with_pyvenv_cfg() {
304+
let root = tempdir().unwrap();
305+
let env_path = root.path().join("my-venv");
306+
let bin_dir = if cfg!(windows) {
307+
env_path.join("Scripts")
308+
} else {
309+
env_path.join("bin")
310+
};
311+
fs::create_dir_all(&bin_dir).unwrap();
312+
let exe = if cfg!(windows) {
313+
bin_dir.join("python.exe")
314+
} else {
315+
bin_dir.join("python")
316+
};
317+
fs::write(&exe, b"").unwrap();
318+
fs::write(
319+
env_path.join("pyvenv.cfg"),
320+
"version = 3.12.0\nhome = /usr/bin\n",
321+
)
322+
.unwrap();
323+
324+
let result = get_virtual_env_environment(&exe, &env_path, &None).unwrap();
325+
326+
assert_eq!(result.kind, Some(PythonEnvironmentKind::PyenvVirtualEnv));
327+
assert_eq!(result.version, Some("3.12.0".to_string()));
328+
assert_eq!(result.executable, Some(exe));
329+
assert_eq!(result.prefix, Some(env_path));
330+
}
331+
}

0 commit comments

Comments
 (0)