Skip to content

Commit 6072b60

Browse files
authored
test: add Linux global Python coverage (#428)
Summary: - Add focused unit tests for Linux global Python environment construction, architecture mapping, symlink handling, and early rejection paths. - Reject non-global paths before hydrating the Linux global cache to avoid unnecessary global-bin scans. Validation: - cargo fmt --all - wsl bash -lc 'cd /mnt/c/GIT/projects/python-environment-tools && cargo test -p pet-linux-global-python' - wsl bash -lc 'cd /mnt/c/GIT/projects/python-environment-tools && cargo clippy --all -- -D warnings' Refs #389
1 parent 1c28b88 commit 6072b60

3 files changed

Lines changed: 183 additions & 2 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-linux-global-python/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ pet-python-utils = { path = "../pet-python-utils" }
1313
pet-virtualenv = { path = "../pet-virtualenv" }
1414
pet-fs = { path = "../pet-fs" }
1515
log = "0.4.21"
16+
17+
[dev-dependencies]
18+
tempfile = "3.13"

crates/pet-linux-global-python/src/lib.rs

Lines changed: 179 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,6 @@ impl Locator for LinuxGlobalPython {
8585
env.version.clone()?;
8686
let executable = env.executable.clone();
8787

88-
self.find_cached(None);
89-
9088
// Resolve the canonical path once — used for both the path guard and cache fallback.
9189
let canonical = fs::canonicalize(&executable).ok();
9290

@@ -100,6 +98,8 @@ impl Locator for LinuxGlobalPython {
10098
return None;
10199
}
102100

101+
self.find_cached(None);
102+
103103
// Try direct cache lookup first.
104104
if let Some(env) = self.reported_executables.get(&executable) {
105105
return Some(env);
@@ -262,3 +262,180 @@ fn get_python_in_bin(env: &PythonEnv, is_64bit: bool) -> Option<PythonEnvironmen
262262
.build(),
263263
)
264264
}
265+
266+
#[cfg(test)]
267+
mod tests {
268+
use super::*;
269+
use pet_core::python_environment::PythonEnvironmentKind;
270+
use std::fs;
271+
use tempfile::tempdir;
272+
273+
#[cfg(windows)]
274+
const PYTHON_EXE: &str = "python.exe";
275+
#[cfg(not(windows))]
276+
const PYTHON_EXE: &str = "python";
277+
278+
#[cfg(windows)]
279+
const PYTHON_VERSIONED_EXE: &str = "python3.12.exe";
280+
#[cfg(not(windows))]
281+
const PYTHON_VERSIONED_EXE: &str = "python3.12";
282+
283+
fn create_executable(path: &Path) {
284+
fs::write(path, b"").unwrap();
285+
}
286+
287+
fn create_env(executable: PathBuf, prefix: PathBuf) -> PythonEnv {
288+
PythonEnv::new(executable, Some(prefix), Some("3.12.1".to_string()))
289+
}
290+
291+
#[test]
292+
fn get_python_in_bin_requires_version_and_prefix() {
293+
let dir = tempdir().unwrap();
294+
let executable = dir.path().join(PYTHON_EXE);
295+
let versionless = PythonEnv::new(executable.clone(), Some(dir.path().to_path_buf()), None);
296+
let prefixless = PythonEnv::new(executable, None, Some("3.12.1".to_string()));
297+
298+
assert!(get_python_in_bin(&versionless, true).is_none());
299+
assert!(get_python_in_bin(&prefixless, true).is_none());
300+
}
301+
302+
#[test]
303+
fn get_python_in_bin_builds_linux_global_environment() {
304+
let dir = tempdir().unwrap();
305+
let executable = dir.path().join(PYTHON_EXE);
306+
create_executable(&executable);
307+
let env = create_env(executable.clone(), dir.path().to_path_buf());
308+
let expected_executable = env.executable.clone();
309+
let expected_prefix = env.prefix.clone();
310+
311+
let environment = get_python_in_bin(&env, true).unwrap();
312+
313+
assert_eq!(environment.kind, Some(PythonEnvironmentKind::LinuxGlobal));
314+
assert_eq!(environment.executable, Some(expected_executable.clone()));
315+
assert_eq!(environment.prefix, expected_prefix);
316+
assert_eq!(environment.version, Some("3.12.1".to_string()));
317+
assert_eq!(environment.arch, Some(Architecture::X64));
318+
assert!(environment.symlinks.unwrap().contains(&expected_executable));
319+
}
320+
321+
#[test]
322+
fn get_python_in_bin_reports_x86_when_not_64_bit() {
323+
let dir = tempdir().unwrap();
324+
let executable = dir.path().join(PYTHON_EXE);
325+
create_executable(&executable);
326+
let env = create_env(executable, dir.path().to_path_buf());
327+
328+
let environment = get_python_in_bin(&env, false).unwrap();
329+
330+
assert_eq!(environment.arch, Some(Architecture::X86));
331+
}
332+
333+
#[test]
334+
fn get_python_in_bin_preserves_and_dedupes_known_symlinks() {
335+
let dir = tempdir().unwrap();
336+
let executable = dir.path().join(PYTHON_EXE);
337+
let known_symlink = dir.path().join(PYTHON_VERSIONED_EXE);
338+
create_executable(&executable);
339+
create_executable(&known_symlink);
340+
let mut env = create_env(executable.clone(), dir.path().to_path_buf());
341+
env.symlinks = Some(vec![known_symlink.clone(), executable.clone()]);
342+
343+
let environment = get_python_in_bin(&env, true).unwrap();
344+
let symlinks = environment.symlinks.unwrap();
345+
346+
assert_eq!(
347+
symlinks.iter().filter(|path| *path == &executable).count(),
348+
1
349+
);
350+
assert!(symlinks.contains(&known_symlink));
351+
}
352+
353+
#[cfg(unix)]
354+
#[test]
355+
fn get_python_in_bin_collects_same_directory_symlink_target() {
356+
use std::os::unix::fs::symlink;
357+
358+
let dir = tempdir().unwrap();
359+
let executable = dir.path().join("python3");
360+
let versioned_executable = dir.path().join(PYTHON_VERSIONED_EXE);
361+
create_executable(&versioned_executable);
362+
symlink(&versioned_executable, &executable).unwrap();
363+
let env = create_env(executable.clone(), dir.path().to_path_buf());
364+
365+
let environment = get_python_in_bin(&env, true).unwrap();
366+
let symlinks = environment.symlinks.unwrap();
367+
368+
assert!(symlinks.contains(&executable));
369+
assert!(symlinks.contains(&versioned_executable));
370+
}
371+
372+
#[cfg(unix)]
373+
#[test]
374+
fn get_python_in_bin_keeps_cross_directory_symlink_separate() {
375+
use std::os::unix::fs::symlink;
376+
377+
let link_dir = tempdir().unwrap();
378+
let real_dir = tempdir().unwrap();
379+
let executable = link_dir.path().join("python3");
380+
let real_executable = real_dir.path().join(PYTHON_VERSIONED_EXE);
381+
create_executable(&real_executable);
382+
symlink(&real_executable, &executable).unwrap();
383+
let env = create_env(executable.clone(), link_dir.path().to_path_buf());
384+
385+
let environment = get_python_in_bin(&env, true).unwrap();
386+
let symlinks = environment.symlinks.unwrap();
387+
388+
assert!(symlinks.contains(&executable));
389+
assert!(!symlinks.contains(&real_executable));
390+
}
391+
392+
#[test]
393+
fn try_from_returns_none_without_version_before_cache_lookup() {
394+
let locator = LinuxGlobalPython::new();
395+
let env = PythonEnv::new(
396+
PathBuf::from("/usr/bin/python3"),
397+
Some(PathBuf::from("/usr")),
398+
None,
399+
);
400+
401+
assert!(locator.try_from(&env).is_none());
402+
assert!(locator.reported_executables.is_empty());
403+
}
404+
405+
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
406+
#[test]
407+
fn try_from_rejects_non_global_path_before_cache_lookup() {
408+
let dir = tempdir().unwrap();
409+
let executable = dir.path().join("python");
410+
create_executable(&executable);
411+
let locator = LinuxGlobalPython::new();
412+
let env = PythonEnv::new(
413+
executable,
414+
Some(dir.path().to_path_buf()),
415+
Some("3.12.1".to_string()),
416+
);
417+
418+
assert!(locator.try_from(&env).is_none());
419+
assert!(locator.reported_executables.is_empty());
420+
}
421+
422+
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
423+
#[test]
424+
fn try_from_rejects_virtualenv_before_cache_lookup() {
425+
let dir = tempdir().unwrap();
426+
let bin_dir = dir.path().join("bin");
427+
fs::create_dir_all(&bin_dir).unwrap();
428+
fs::write(bin_dir.join("activate"), b"").unwrap();
429+
let executable = bin_dir.join("python");
430+
create_executable(&executable);
431+
let locator = LinuxGlobalPython::new();
432+
let env = PythonEnv::new(
433+
executable,
434+
Some(dir.path().to_path_buf()),
435+
Some("3.12.1".to_string()),
436+
);
437+
438+
assert!(locator.try_from(&env).is_none());
439+
assert!(locator.reported_executables.is_empty());
440+
}
441+
}

0 commit comments

Comments
 (0)