Skip to content

Commit 2e89130

Browse files
committed
test: add Linux global Python coverage (Refs #389)
1 parent 1c28b88 commit 2e89130

3 files changed

Lines changed: 181 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: 177 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,178 @@ 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+
309+
let environment = get_python_in_bin(&env, true).unwrap();
310+
311+
assert_eq!(environment.kind, Some(PythonEnvironmentKind::LinuxGlobal));
312+
assert_eq!(environment.executable, Some(executable.clone()));
313+
assert_eq!(environment.prefix, Some(dir.path().to_path_buf()));
314+
assert_eq!(environment.version, Some("3.12.1".to_string()));
315+
assert_eq!(environment.arch, Some(Architecture::X64));
316+
assert!(environment.symlinks.unwrap().contains(&executable));
317+
}
318+
319+
#[test]
320+
fn get_python_in_bin_reports_x86_when_not_64_bit() {
321+
let dir = tempdir().unwrap();
322+
let executable = dir.path().join(PYTHON_EXE);
323+
create_executable(&executable);
324+
let env = create_env(executable, dir.path().to_path_buf());
325+
326+
let environment = get_python_in_bin(&env, false).unwrap();
327+
328+
assert_eq!(environment.arch, Some(Architecture::X86));
329+
}
330+
331+
#[test]
332+
fn get_python_in_bin_preserves_and_dedupes_known_symlinks() {
333+
let dir = tempdir().unwrap();
334+
let executable = dir.path().join(PYTHON_EXE);
335+
let known_symlink = dir.path().join(PYTHON_VERSIONED_EXE);
336+
create_executable(&executable);
337+
create_executable(&known_symlink);
338+
let mut env = create_env(executable.clone(), dir.path().to_path_buf());
339+
env.symlinks = Some(vec![known_symlink.clone(), executable.clone()]);
340+
341+
let environment = get_python_in_bin(&env, true).unwrap();
342+
let symlinks = environment.symlinks.unwrap();
343+
344+
assert_eq!(
345+
symlinks.iter().filter(|path| *path == &executable).count(),
346+
1
347+
);
348+
assert!(symlinks.contains(&known_symlink));
349+
}
350+
351+
#[cfg(unix)]
352+
#[test]
353+
fn get_python_in_bin_collects_same_directory_symlink_target() {
354+
use std::os::unix::fs::symlink;
355+
356+
let dir = tempdir().unwrap();
357+
let executable = dir.path().join("python3");
358+
let versioned_executable = dir.path().join(PYTHON_VERSIONED_EXE);
359+
create_executable(&versioned_executable);
360+
symlink(&versioned_executable, &executable).unwrap();
361+
let env = create_env(executable.clone(), dir.path().to_path_buf());
362+
363+
let environment = get_python_in_bin(&env, true).unwrap();
364+
let symlinks = environment.symlinks.unwrap();
365+
366+
assert!(symlinks.contains(&executable));
367+
assert!(symlinks.contains(&versioned_executable));
368+
}
369+
370+
#[cfg(unix)]
371+
#[test]
372+
fn get_python_in_bin_keeps_cross_directory_symlink_separate() {
373+
use std::os::unix::fs::symlink;
374+
375+
let link_dir = tempdir().unwrap();
376+
let real_dir = tempdir().unwrap();
377+
let executable = link_dir.path().join("python3");
378+
let real_executable = real_dir.path().join(PYTHON_VERSIONED_EXE);
379+
create_executable(&real_executable);
380+
symlink(&real_executable, &executable).unwrap();
381+
let env = create_env(executable.clone(), link_dir.path().to_path_buf());
382+
383+
let environment = get_python_in_bin(&env, true).unwrap();
384+
let symlinks = environment.symlinks.unwrap();
385+
386+
assert!(symlinks.contains(&executable));
387+
assert!(!symlinks.contains(&real_executable));
388+
}
389+
390+
#[test]
391+
fn try_from_returns_none_without_version_before_cache_lookup() {
392+
let locator = LinuxGlobalPython::new();
393+
let env = PythonEnv::new(
394+
PathBuf::from("/usr/bin/python3"),
395+
Some(PathBuf::from("/usr")),
396+
None,
397+
);
398+
399+
assert!(locator.try_from(&env).is_none());
400+
assert!(locator.reported_executables.is_empty());
401+
}
402+
403+
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
404+
#[test]
405+
fn try_from_rejects_non_global_path_before_cache_lookup() {
406+
let dir = tempdir().unwrap();
407+
let executable = dir.path().join("python");
408+
create_executable(&executable);
409+
let locator = LinuxGlobalPython::new();
410+
let env = PythonEnv::new(
411+
executable,
412+
Some(dir.path().to_path_buf()),
413+
Some("3.12.1".to_string()),
414+
);
415+
416+
assert!(locator.try_from(&env).is_none());
417+
assert!(locator.reported_executables.is_empty());
418+
}
419+
420+
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
421+
#[test]
422+
fn try_from_rejects_virtualenv_before_cache_lookup() {
423+
let dir = tempdir().unwrap();
424+
let bin_dir = dir.path().join("bin");
425+
fs::create_dir_all(&bin_dir).unwrap();
426+
fs::write(bin_dir.join("activate"), b"").unwrap();
427+
let executable = bin_dir.join("python");
428+
create_executable(&executable);
429+
let locator = LinuxGlobalPython::new();
430+
let env = PythonEnv::new(
431+
executable,
432+
Some(dir.path().to_path_buf()),
433+
Some("3.12.1".to_string()),
434+
);
435+
436+
assert!(locator.try_from(&env).is_none());
437+
assert!(locator.reported_executables.is_empty());
438+
}
439+
}

0 commit comments

Comments
 (0)