Skip to content

Commit 52eea98

Browse files
authored
test: add unit tests for pet-pyenv crate (Fixes #389) (#441)
Add 22 new unit tests to pet-pyenv (27 total, up from 5 integration tests): - **environments.rs** (14 tests): get_version parsing (stable, dev, alpha, win32, non-version, partial, rc limitation), get_generic_python_environment (version extraction, win32 arch, no-arch, manager, unrecognized folder), get_virtual_env_environment (with/without pyvenv.cfg) - **environment_locations.rs** (8 tests): get_pyenv_dir (PYENV_ROOT priority, PYENV fallback, no vars), get_home_pyenv_dir (no home, expected path), get_binary_from_known_paths (found, not found, empty) Fixes #389
1 parent 81577e3 commit 52eea98

4 files changed

Lines changed: 350 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: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,244 @@ 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!(
179+
result.executable.as_ref().unwrap().file_name(),
180+
exe.file_name()
181+
);
182+
assert_eq!(result.version, Some("3.12.0".to_string()));
183+
assert_eq!(
184+
result.prefix.as_ref().unwrap().file_name(),
185+
env_path.file_name()
186+
);
187+
assert!(result.manager.is_none());
188+
}
189+
190+
#[test]
191+
fn get_generic_python_environment_with_win32_folder_sets_x86_arch() {
192+
let root = tempdir().unwrap();
193+
let env_path = root.path().join("3.11.0a4-win32");
194+
let bin_dir = if cfg!(windows) {
195+
env_path.join("Scripts")
196+
} else {
197+
env_path.join("bin")
198+
};
199+
fs::create_dir_all(&bin_dir).unwrap();
200+
let exe = if cfg!(windows) {
201+
bin_dir.join("python.exe")
202+
} else {
203+
bin_dir.join("python")
204+
};
205+
fs::write(&exe, b"").unwrap();
206+
207+
let result = get_generic_python_environment(&exe, &env_path, &None).unwrap();
208+
209+
assert_eq!(result.arch, Some(Architecture::X86));
210+
}
211+
212+
#[test]
213+
fn get_generic_python_environment_with_non_win32_folder_has_no_arch() {
214+
let root = tempdir().unwrap();
215+
let env_path = root.path().join("3.12.0");
216+
let bin_dir = if cfg!(windows) {
217+
env_path.join("Scripts")
218+
} else {
219+
env_path.join("bin")
220+
};
221+
fs::create_dir_all(&bin_dir).unwrap();
222+
let exe = if cfg!(windows) {
223+
bin_dir.join("python.exe")
224+
} else {
225+
bin_dir.join("python")
226+
};
227+
fs::write(&exe, b"").unwrap();
228+
229+
let result = get_generic_python_environment(&exe, &env_path, &None).unwrap();
230+
231+
assert!(result.arch.is_none());
232+
}
233+
234+
#[test]
235+
fn get_generic_python_environment_includes_manager_when_provided() {
236+
let root = tempdir().unwrap();
237+
let env_path = root.path().join("3.12.0");
238+
let bin_dir = if cfg!(windows) {
239+
env_path.join("Scripts")
240+
} else {
241+
env_path.join("bin")
242+
};
243+
fs::create_dir_all(&bin_dir).unwrap();
244+
let exe = if cfg!(windows) {
245+
bin_dir.join("python.exe")
246+
} else {
247+
bin_dir.join("python")
248+
};
249+
fs::write(&exe, b"").unwrap();
250+
251+
let mgr = EnvManager::new(
252+
PathBuf::from("/usr/bin/pyenv"),
253+
pet_core::manager::EnvManagerType::Pyenv,
254+
Some("2.4.0".to_string()),
255+
);
256+
let result = get_generic_python_environment(&exe, &env_path, &Some(mgr.clone())).unwrap();
257+
258+
assert_eq!(result.manager, Some(mgr));
259+
}
260+
261+
#[test]
262+
fn get_generic_python_environment_with_unrecognized_folder_name() {
263+
let root = tempdir().unwrap();
264+
let env_path = root.path().join("mambaforge-4.10.1-4");
265+
let bin_dir = if cfg!(windows) {
266+
env_path.join("Scripts")
267+
} else {
268+
env_path.join("bin")
269+
};
270+
fs::create_dir_all(&bin_dir).unwrap();
271+
let exe = if cfg!(windows) {
272+
bin_dir.join("python.exe")
273+
} else {
274+
bin_dir.join("python")
275+
};
276+
fs::write(&exe, b"").unwrap();
277+
278+
let result = get_generic_python_environment(&exe, &env_path, &None).unwrap();
279+
280+
assert_eq!(result.kind, Some(PythonEnvironmentKind::Pyenv));
281+
// No version extractable from folder name and no header files
282+
assert!(result.version.is_none());
283+
}
284+
285+
// get_virtual_env_environment tests
286+
#[test]
287+
fn get_virtual_env_returns_none_without_pyvenv_cfg() {
288+
let root = tempdir().unwrap();
289+
let env_path = root.path().join("my-venv");
290+
let bin_dir = if cfg!(windows) {
291+
env_path.join("Scripts")
292+
} else {
293+
env_path.join("bin")
294+
};
295+
fs::create_dir_all(&bin_dir).unwrap();
296+
let exe = if cfg!(windows) {
297+
bin_dir.join("python.exe")
298+
} else {
299+
bin_dir.join("python")
300+
};
301+
fs::write(&exe, b"").unwrap();
302+
303+
let result = get_virtual_env_environment(&exe, &env_path, &None);
304+
305+
assert!(result.is_none());
306+
}
307+
308+
#[test]
309+
fn get_virtual_env_returns_env_with_pyvenv_cfg() {
310+
let root = tempdir().unwrap();
311+
let env_path = root.path().join("my-venv");
312+
let bin_dir = if cfg!(windows) {
313+
env_path.join("Scripts")
314+
} else {
315+
env_path.join("bin")
316+
};
317+
fs::create_dir_all(&bin_dir).unwrap();
318+
let exe = if cfg!(windows) {
319+
bin_dir.join("python.exe")
320+
} else {
321+
bin_dir.join("python")
322+
};
323+
fs::write(&exe, b"").unwrap();
324+
fs::write(
325+
env_path.join("pyvenv.cfg"),
326+
"version = 3.12.0\nhome = /usr/bin\n",
327+
)
328+
.unwrap();
329+
330+
let result = get_virtual_env_environment(&exe, &env_path, &None).unwrap();
331+
332+
assert_eq!(result.kind, Some(PythonEnvironmentKind::PyenvVirtualEnv));
333+
assert_eq!(result.version, Some("3.12.0".to_string()));
334+
assert_eq!(
335+
result.executable.as_ref().unwrap().file_name(),
336+
exe.file_name()
337+
);
338+
assert_eq!(
339+
result.prefix.as_ref().unwrap().file_name(),
340+
env_path.file_name()
341+
);
342+
}
343+
}

0 commit comments

Comments
 (0)