Skip to content

Commit 81577e3

Browse files
authored
test: add unit tests for pet-homebrew crate (Fixes #389) (#440)
Add 21 new unit tests to pet-homebrew (31 total, up from 10): - **environments.rs**: version extraction (3), get_prefix always None (1), get_python_info across all prefix types (4) - **sym_links.rs**: is_homebrew_python edge cases (2), get_known_symlinks_impl for all 3 prefix types (3), version regex mismatch (1), linuxbrew expected paths (1) - **lib.rs**: try_from for opt/homebrew (1), usr/local/Cellar (1), conda rejection via parent/grandparent/prefix (3) - **environment_locations.rs**: no-env-var behavior (1), dedup validation (1) All tests properly gated with \#[cfg(all(test, unix))]\ for platform safety. Fixes #389
1 parent 83e7867 commit 81577e3

4 files changed

Lines changed: 305 additions & 1 deletion

File tree

crates/pet-homebrew/src/environment_locations.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,43 @@ mod tests {
9191
.iter()
9292
.any(|path| path == &missing_homebrew_prefix.join("bin")));
9393
}
94+
95+
#[test]
96+
fn homebrew_prefix_bin_returns_results_without_env_var() {
97+
let env_vars = EnvVariables {
98+
home: None,
99+
root: None,
100+
path: None,
101+
homebrew_prefix: None,
102+
known_global_search_locations: vec![],
103+
};
104+
105+
// Should not panic and should return whatever standard paths exist
106+
let prefix_bins = get_homebrew_prefix_bin(&env_vars);
107+
// All returned paths should actually exist
108+
for path in &prefix_bins {
109+
assert!(path.exists(), "{:?} should exist", path);
110+
}
111+
}
112+
113+
#[test]
114+
fn homebrew_prefix_bin_does_not_duplicate_when_env_var_matches_existing_dir() {
115+
// Create a temp dir to act as a custom homebrew prefix.
116+
// Call get_homebrew_prefix_bin twice with the same prefix to ensure
117+
// the env var path only appears once in the result.
118+
let custom_prefix = tempdir().unwrap();
119+
let custom_bin = custom_prefix.path().join("bin");
120+
fs::create_dir_all(&custom_bin).unwrap();
121+
let env_vars = EnvVariables {
122+
home: None,
123+
root: None,
124+
path: None,
125+
homebrew_prefix: Some(custom_prefix.path().to_string_lossy().to_string()),
126+
known_global_search_locations: vec![],
127+
};
128+
129+
let prefix_bins = get_homebrew_prefix_bin(&env_vars);
130+
let count = prefix_bins.iter().filter(|p| **p == custom_bin).count();
131+
assert_eq!(count, 1, "Custom bin path should appear exactly once");
132+
}
94133
}

crates/pet-homebrew/src/environments.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,4 +167,105 @@ mod tests {
167167
Some("3.11.9".to_string())
168168
);
169169
}
170+
171+
#[test]
172+
fn extract_version_from_opt_homebrew_path() {
173+
assert_eq!(
174+
get_version(&PathBuf::from(
175+
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12"
176+
)),
177+
Some("3.12.3".to_string())
178+
);
179+
}
180+
181+
#[test]
182+
fn extract_version_from_usr_local_cellar_path() {
183+
assert_eq!(
184+
get_version(&PathBuf::from(
185+
"/usr/local/Cellar/python@3.8/3.8.20/Frameworks/Python.framework/Versions/3.8/bin/python3.8"
186+
)),
187+
Some("3.8.20".to_string())
188+
);
189+
}
190+
191+
#[test]
192+
fn extract_version_returns_none_for_path_without_version() {
193+
assert_eq!(get_version(&PathBuf::from("/usr/bin/python3")), None);
194+
}
195+
196+
#[test]
197+
fn get_prefix_always_returns_none() {
198+
assert!(get_prefix(&PathBuf::from(
199+
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12"
200+
))
201+
.is_none());
202+
assert!(get_prefix(&PathBuf::from(
203+
"/home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.4/bin/python3.12"
204+
))
205+
.is_none());
206+
assert!(get_prefix(&PathBuf::from(
207+
"/usr/local/Cellar/python@3.8/3.8.20/bin/python3.8"
208+
))
209+
.is_none());
210+
}
211+
212+
#[test]
213+
fn get_python_info_returns_correct_kind_and_executable() {
214+
let bin_exe = PathBuf::from("/home/linuxbrew/.linuxbrew/bin/python3.12");
215+
let resolved_exe =
216+
PathBuf::from("/home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.4/bin/python3.12");
217+
218+
let env = get_python_info(&bin_exe, &resolved_exe).unwrap();
219+
220+
assert_eq!(env.kind, Some(PythonEnvironmentKind::Homebrew));
221+
assert_eq!(env.executable, Some(bin_exe.clone()));
222+
assert_eq!(env.version, Some("3.12.4".to_string()));
223+
assert_eq!(env.prefix, None);
224+
// Both bin exe and resolved exe should be in symlinks
225+
let symlinks = env.symlinks.unwrap();
226+
assert!(symlinks.contains(&bin_exe));
227+
assert!(symlinks.contains(&resolved_exe));
228+
}
229+
230+
#[test]
231+
fn get_python_info_returns_none_version_for_unversioned_path() {
232+
let bin_exe = PathBuf::from("/home/linuxbrew/.linuxbrew/bin/python3");
233+
let resolved_exe = PathBuf::from("/home/linuxbrew/.linuxbrew/bin/python3");
234+
235+
let env = get_python_info(&bin_exe, &resolved_exe).unwrap();
236+
237+
assert_eq!(env.kind, Some(PythonEnvironmentKind::Homebrew));
238+
assert_eq!(env.version, None);
239+
}
240+
241+
#[test]
242+
fn get_python_info_for_opt_homebrew_path() {
243+
let bin_exe = PathBuf::from("/opt/homebrew/bin/python3.12");
244+
let resolved_exe = PathBuf::from(
245+
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
246+
);
247+
248+
let env = get_python_info(&bin_exe, &resolved_exe).unwrap();
249+
250+
assert_eq!(env.kind, Some(PythonEnvironmentKind::Homebrew));
251+
assert_eq!(env.executable, Some(bin_exe.clone()));
252+
assert_eq!(env.version, Some("3.12.3".to_string()));
253+
let symlinks = env.symlinks.unwrap();
254+
assert!(symlinks.contains(&bin_exe));
255+
assert!(symlinks.contains(&resolved_exe));
256+
}
257+
258+
#[test]
259+
fn get_python_info_for_usr_local_cellar_path() {
260+
let bin_exe = PathBuf::from("/usr/local/bin/python3.8");
261+
let resolved_exe = PathBuf::from(
262+
"/usr/local/Cellar/python@3.8/3.8.20/Frameworks/Python.framework/Versions/3.8/bin/python3.8",
263+
);
264+
265+
let env = get_python_info(&bin_exe, &resolved_exe).unwrap();
266+
267+
assert_eq!(env.kind, Some(PythonEnvironmentKind::Homebrew));
268+
assert_eq!(env.executable, Some(bin_exe));
269+
assert_eq!(env.version, Some("3.8.20".to_string()));
270+
}
170271
}

crates/pet-homebrew/src/lib.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,4 +281,100 @@ mod tests {
281281
);
282282
assert!(locator.try_from(&conda).is_none());
283283
}
284+
285+
#[test]
286+
fn try_from_identifies_opt_homebrew_python() {
287+
let locator = Homebrew::from(&TestEnvironment {
288+
homebrew_prefix: None,
289+
});
290+
let env = PythonEnv::new(
291+
PathBuf::from(
292+
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
293+
),
294+
None,
295+
None,
296+
);
297+
298+
let homebrew_env = locator.try_from(&env).unwrap();
299+
300+
assert_eq!(homebrew_env.kind, Some(PythonEnvironmentKind::Homebrew));
301+
assert_eq!(
302+
homebrew_env.executable,
303+
Some(PathBuf::from("/opt/homebrew/bin/python3.12"))
304+
);
305+
assert_eq!(homebrew_env.version, Some("3.12.3".to_string()));
306+
}
307+
308+
#[test]
309+
fn try_from_identifies_usr_local_cellar_python() {
310+
let locator = Homebrew::from(&TestEnvironment {
311+
homebrew_prefix: None,
312+
});
313+
let env = PythonEnv::new(
314+
PathBuf::from(
315+
"/usr/local/Cellar/python@3.8/3.8.20/Frameworks/Python.framework/Versions/3.8/bin/python3.8",
316+
),
317+
None,
318+
None,
319+
);
320+
321+
let homebrew_env = locator.try_from(&env).unwrap();
322+
323+
assert_eq!(homebrew_env.kind, Some(PythonEnvironmentKind::Homebrew));
324+
assert_eq!(
325+
homebrew_env.executable,
326+
Some(PathBuf::from("/usr/local/bin/python3.8"))
327+
);
328+
assert_eq!(homebrew_env.version, Some("3.8.20".to_string()));
329+
}
330+
331+
#[test]
332+
fn try_from_rejects_conda_env_when_parent_is_conda() {
333+
let locator = Homebrew::from(&TestEnvironment {
334+
homebrew_prefix: None,
335+
});
336+
// Create a directory that looks like a conda env (has conda-meta)
337+
let conda_root = tempdir().unwrap();
338+
fs::create_dir_all(conda_root.path().join("conda-meta")).unwrap();
339+
// Place executable directly in the conda-meta parent directory
340+
let exe = conda_root.path().join("python3.12");
341+
fs::write(&exe, b"").unwrap();
342+
343+
let env = PythonEnv::new(exe, None, None);
344+
assert!(locator.try_from(&env).is_none());
345+
}
346+
347+
#[test]
348+
fn try_from_rejects_conda_env_when_grandparent_is_conda() {
349+
let locator = Homebrew::from(&TestEnvironment {
350+
homebrew_prefix: None,
351+
});
352+
// Create a directory that looks like a conda env (has conda-meta)
353+
let conda_root = tempdir().unwrap();
354+
fs::create_dir_all(conda_root.path().join("conda-meta")).unwrap();
355+
let bin_dir = conda_root.path().join("bin");
356+
fs::create_dir_all(&bin_dir).unwrap();
357+
let exe = bin_dir.join("python3.12");
358+
fs::write(&exe, b"").unwrap();
359+
360+
let env = PythonEnv::new(exe, None, None);
361+
assert!(locator.try_from(&env).is_none());
362+
}
363+
364+
#[test]
365+
fn try_from_rejects_conda_env_via_prefix() {
366+
let locator = Homebrew::from(&TestEnvironment {
367+
homebrew_prefix: None,
368+
});
369+
// Conda env detected via prefix having conda-meta
370+
let conda_root = tempdir().unwrap();
371+
fs::create_dir_all(conda_root.path().join("conda-meta")).unwrap();
372+
let bin_dir = conda_root.path().join("bin");
373+
fs::create_dir_all(&bin_dir).unwrap();
374+
let exe = bin_dir.join("python3.12");
375+
fs::write(&exe, b"").unwrap();
376+
377+
let env = PythonEnv::new(exe, Some(conda_root.path().to_path_buf()), None);
378+
assert!(locator.try_from(&env).is_none());
379+
}
284380
}

crates/pet-homebrew/src/sym_links.rs

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ pub fn get_known_symlinks_impl(
244244
}
245245
}
246246

247-
#[cfg(test)]
247+
#[cfg(all(test, unix))]
248248
mod tests {
249249
use super::*;
250250

@@ -262,6 +262,29 @@ mod tests {
262262
assert!(!is_homebrew_python(Path::new("/usr/bin/python3.12")));
263263
}
264264

265+
#[test]
266+
fn is_homebrew_python_recognizes_opt_homebrew_bin_paths() {
267+
assert!(is_homebrew_python(Path::new(
268+
"/opt/homebrew/bin/python3.12"
269+
)));
270+
assert!(is_homebrew_python(Path::new(
271+
"/opt/homebrew/opt/python@3.12/bin/python3.12"
272+
)));
273+
assert!(is_homebrew_python(Path::new(
274+
"/opt/homebrew/Frameworks/Python.framework/Versions/3.12/bin/python3.12"
275+
)));
276+
}
277+
278+
#[test]
279+
fn is_homebrew_python_rejects_non_homebrew_paths() {
280+
assert!(!is_homebrew_python(Path::new("/usr/local/bin/python3.12")));
281+
assert!(!is_homebrew_python(Path::new("/usr/bin/python3")));
282+
assert!(!is_homebrew_python(Path::new(
283+
"/home/user/.pyenv/versions/3.12.0/bin/python3.12"
284+
)));
285+
assert!(!is_homebrew_python(Path::new("")));
286+
}
287+
265288
#[test]
266289
fn known_symlink_templates_include_resolved_executable_for_linuxbrew() {
267290
let resolved_exe =
@@ -278,4 +301,49 @@ mod tests {
278301
.is_empty()
279302
);
280303
}
304+
305+
#[test]
306+
fn known_symlink_templates_include_self_for_opt_homebrew() {
307+
let resolved_exe = PathBuf::from(
308+
"/opt/homebrew/Cellar/python@3.12/3.12.3/Frameworks/Python.framework/Versions/3.12/bin/python3.12",
309+
);
310+
let symlinks = get_known_symlinks_impl(&resolved_exe, &"3.12.3".to_string());
311+
312+
assert!(symlinks.contains(&resolved_exe));
313+
assert!(symlinks.len() >= 1);
314+
}
315+
316+
#[test]
317+
fn known_symlink_templates_include_self_for_usr_local_cellar() {
318+
let resolved_exe = PathBuf::from(
319+
"/usr/local/Cellar/python@3.8/3.8.20/Frameworks/Python.framework/Versions/3.8/bin/python3.8",
320+
);
321+
let symlinks = get_known_symlinks_impl(&resolved_exe, &"3.8.20".to_string());
322+
323+
assert!(symlinks.contains(&resolved_exe));
324+
assert!(symlinks.len() >= 1);
325+
}
326+
327+
#[test]
328+
fn known_symlink_templates_return_empty_when_version_regex_does_not_match() {
329+
// Path under /opt/homebrew but without a python@version segment
330+
let resolved_exe = PathBuf::from("/opt/homebrew/bin/python3.12");
331+
let symlinks = get_known_symlinks_impl(&resolved_exe, &"3.12.0".to_string());
332+
333+
// No python@version/ in path, so regex won't capture → returns empty
334+
assert!(symlinks.is_empty());
335+
}
336+
337+
#[test]
338+
fn known_symlink_templates_for_linuxbrew_contain_expected_paths() {
339+
let resolved_exe =
340+
PathBuf::from("/home/linuxbrew/.linuxbrew/Cellar/python@3.12/3.12.4/bin/python3.12");
341+
let symlinks = get_known_symlinks_impl(&resolved_exe, &"3.12.4".to_string());
342+
343+
// The resolved exe itself is always included
344+
assert!(symlinks.contains(&resolved_exe));
345+
// On a test system without real symlinks, only the resolved exe will pass validation.
346+
// But verify the function doesn't panic and returns at least the resolved exe.
347+
assert!(!symlinks.is_empty());
348+
}
281349
}

0 commit comments

Comments
 (0)