Skip to content

Commit 9d3b46c

Browse files
committed
test: improve locator coverage (Fixes #389)
1 parent d3a060f commit 9d3b46c

File tree

17 files changed

+1703
-56
lines changed

17 files changed

+1703
-56
lines changed

crates/pet-core/src/python_environment.rs

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -415,13 +415,16 @@ pub fn get_environment_key(env: &PythonEnvironment) -> Option<PathBuf> {
415415
if let Some(exe) = &env.executable {
416416
Some(exe.clone())
417417
} else if let Some(prefix) = &env.prefix {
418-
// If this is a conda env without Python, then the exe will be prefix/bin/python
418+
// If this is a conda env without Python, use the platform's default interpreter path.
419419
if env.kind == Some(PythonEnvironmentKind::Conda) {
420-
Some(prefix.join("bin").join(if cfg!(windows) {
421-
"python.exe"
422-
} else {
423-
"python"
424-
}))
420+
#[cfg(windows)]
421+
{
422+
Some(prefix.join("python.exe"))
423+
}
424+
#[cfg(not(windows))]
425+
{
426+
Some(prefix.join("bin").join("python"))
427+
}
425428
} else {
426429
Some(prefix.clone())
427430
}
@@ -436,11 +439,12 @@ pub fn get_environment_key(env: &PythonEnvironment) -> Option<PathBuf> {
436439

437440
#[cfg(test)]
438441
mod tests {
439-
#[cfg(windows)]
440-
use super::{get_shortest_executable, PythonEnvironmentKind};
441-
#[cfg(windows)]
442+
use super::{get_environment_key, PythonEnvironment, PythonEnvironmentKind};
442443
use std::path::PathBuf;
443444

445+
#[cfg(windows)]
446+
use super::get_shortest_executable;
447+
444448
#[test]
445449
#[cfg(windows)]
446450
fn shorted_exe_path_windows_store() {
@@ -459,4 +463,64 @@ mod tests {
459463
))
460464
);
461465
}
466+
467+
#[test]
468+
fn environment_key_uses_executable_when_available() {
469+
let executable = PathBuf::from(if cfg!(windows) {
470+
r"C:\env\Scripts\python.exe"
471+
} else {
472+
"/env/bin/python"
473+
});
474+
let prefix = PathBuf::from(if cfg!(windows) { r"C:\env" } else { "/env" });
475+
let environment = PythonEnvironment {
476+
executable: Some(executable.clone()),
477+
prefix: Some(prefix),
478+
kind: Some(PythonEnvironmentKind::Venv),
479+
..Default::default()
480+
};
481+
482+
assert_eq!(get_environment_key(&environment), Some(executable));
483+
}
484+
485+
#[test]
486+
fn environment_key_uses_conda_default_python_when_executable_is_missing() {
487+
let prefix = PathBuf::from(if cfg!(windows) {
488+
r"C:\conda-env"
489+
} else {
490+
"/conda-env"
491+
});
492+
let environment = PythonEnvironment {
493+
executable: None,
494+
prefix: Some(prefix.clone()),
495+
kind: Some(PythonEnvironmentKind::Conda),
496+
..Default::default()
497+
};
498+
499+
assert_eq!(
500+
get_environment_key(&environment),
501+
Some(if cfg!(windows) {
502+
prefix.join("python.exe")
503+
} else {
504+
prefix.join("bin").join("python")
505+
})
506+
);
507+
}
508+
509+
#[test]
510+
fn environment_key_uses_non_conda_prefix_when_executable_is_missing() {
511+
let prefix = PathBuf::from(if cfg!(windows) { r"C:\env" } else { "/env" });
512+
let environment = PythonEnvironment {
513+
executable: None,
514+
prefix: Some(prefix.clone()),
515+
kind: Some(PythonEnvironmentKind::Venv),
516+
..Default::default()
517+
};
518+
519+
assert_eq!(get_environment_key(&environment), Some(prefix));
520+
}
521+
522+
#[test]
523+
fn environment_key_returns_none_without_executable_or_prefix() {
524+
assert_eq!(get_environment_key(&PythonEnvironment::default()), None);
525+
}
462526
}

crates/pet-env-var-path/src/lib.rs

Lines changed: 126 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,47 @@
33

44
use pet_core::os_environment::Environment;
55
use std::collections::HashSet;
6-
use std::path::PathBuf;
6+
use std::path::{Path, PathBuf};
77

88
pub fn get_search_paths_from_env_variables(environment: &dyn Environment) -> Vec<PathBuf> {
9-
// Exclude files from this folder, as they would have been discovered elsewhere (widows_store)
9+
let search_paths = environment
10+
.get_know_global_search_locations()
11+
.into_iter()
12+
.map(normalize_search_path)
13+
.collect::<HashSet<PathBuf>>();
14+
15+
// Exclude files from this folder, as they would have been discovered elsewhere (windows_store)
1016
// Also the exe is merely a pointer to another file.
11-
if let Some(home) = environment.get_user_home() {
17+
let user_home = environment.get_user_home();
18+
search_paths
19+
.into_iter()
20+
.filter(|search_path| !is_windows_apps_path(search_path, user_home.as_ref()))
21+
.collect()
22+
}
23+
24+
fn is_windows_apps_path(search_path: &Path, user_home: Option<&PathBuf>) -> bool {
25+
if let Some(home) = user_home {
1226
let apps_path = home
1327
.join("AppData")
1428
.join("Local")
1529
.join("Microsoft")
1630
.join("WindowsApps");
17-
18-
environment
19-
.get_know_global_search_locations()
20-
.into_iter()
21-
.map(normalize_search_path)
22-
.collect::<HashSet<PathBuf>>()
23-
.into_iter()
24-
.filter(|p| !p.starts_with(apps_path.clone()))
25-
.collect()
26-
} else {
27-
Vec::new()
31+
if search_path.starts_with(apps_path) {
32+
return true;
33+
}
2834
}
35+
36+
let components = search_path
37+
.components()
38+
.map(|component| component.as_os_str().to_string_lossy())
39+
.collect::<Vec<_>>();
40+
41+
components.windows(4).any(|components| {
42+
components[0].eq_ignore_ascii_case("AppData")
43+
&& components[1].eq_ignore_ascii_case("Local")
44+
&& components[2].eq_ignore_ascii_case("Microsoft")
45+
&& components[3].eq_ignore_ascii_case("WindowsApps")
46+
})
2947
}
3048

3149
/// Normalizes a search path for deduplication purposes.
@@ -52,3 +70,97 @@ fn normalize_search_path(path: PathBuf) -> PathBuf {
5270
pet_fs::path::norm_case(&path)
5371
}
5472
}
73+
74+
#[cfg(test)]
75+
mod tests {
76+
use super::*;
77+
use std::{
78+
fs,
79+
time::{SystemTime, UNIX_EPOCH},
80+
};
81+
82+
struct TestEnvironment {
83+
user_home: Option<PathBuf>,
84+
global_search_locations: Vec<PathBuf>,
85+
}
86+
87+
impl Environment for TestEnvironment {
88+
fn get_user_home(&self) -> Option<PathBuf> {
89+
self.user_home.clone()
90+
}
91+
92+
fn get_root(&self) -> Option<PathBuf> {
93+
None
94+
}
95+
96+
fn get_env_var(&self, _key: String) -> Option<String> {
97+
None
98+
}
99+
100+
fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
101+
self.global_search_locations.clone()
102+
}
103+
}
104+
105+
fn create_test_dir(name: &str) -> PathBuf {
106+
let unique = SystemTime::now()
107+
.duration_since(UNIX_EPOCH)
108+
.unwrap()
109+
.as_nanos();
110+
let directory = std::env::temp_dir().join(format!(
111+
"pet-env-var-path-{name}-{}-{unique}",
112+
std::process::id()
113+
));
114+
fs::create_dir_all(&directory).unwrap();
115+
directory
116+
}
117+
118+
#[test]
119+
fn search_paths_are_deduplicated_and_windows_apps_paths_are_filtered() {
120+
let home = create_test_dir("home");
121+
let regular_path = home.join("Python");
122+
let windows_apps_path = home
123+
.join("AppData")
124+
.join("Local")
125+
.join("Microsoft")
126+
.join("WindowsApps");
127+
fs::create_dir_all(&regular_path).unwrap();
128+
fs::create_dir_all(&windows_apps_path).unwrap();
129+
130+
let environment = TestEnvironment {
131+
user_home: Some(home.clone()),
132+
global_search_locations: vec![
133+
regular_path.clone(),
134+
regular_path.clone(),
135+
windows_apps_path,
136+
],
137+
};
138+
139+
let mut search_paths = get_search_paths_from_env_variables(&environment);
140+
search_paths.sort();
141+
142+
assert_eq!(search_paths, vec![normalize_search_path(regular_path)]);
143+
144+
fs::remove_dir_all(home).unwrap();
145+
}
146+
147+
#[test]
148+
fn search_paths_are_preserved_when_home_is_unknown() {
149+
let environment = TestEnvironment {
150+
user_home: None,
151+
global_search_locations: vec![
152+
PathBuf::from("/usr/bin"),
153+
PathBuf::from(if cfg!(windows) {
154+
r"C:\Users\User\AppData\Local\Microsoft\WindowsApps"
155+
} else {
156+
"/Users/user/AppData/Local/Microsoft/WindowsApps"
157+
}),
158+
],
159+
};
160+
161+
assert_eq!(
162+
get_search_paths_from_env_variables(&environment),
163+
vec![normalize_search_path(PathBuf::from("/usr/bin"))]
164+
);
165+
}
166+
}

0 commit comments

Comments
 (0)