Skip to content

Commit ce0f808

Browse files
committed
test: add mamba/micromamba unit and CI tests
- Add is_mamba_executable test for cross-platform binary name detection - Add find_mamba_binary/find_micromamba_binary PATH discovery tests - Add CondaManager::from tests for miniforge (conda+mamba) and micromamba-only installs - Add CI test that installs mamba and verifies both managers are reported - Add test fixtures: miniforge3-mamba and micromamba-only directories
1 parent b5b7cc8 commit ce0f808

File tree

19 files changed

+274
-9
lines changed

19 files changed

+274
-9
lines changed

crates/pet-conda/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,10 +349,13 @@ impl Locator for Conda {
349349
reporter.report_environment(&env);
350350

351351
// Also check for a mamba/micromamba manager in the same directory and report it.
352+
let is_new_mamba = !self.mamba_managers.contains_key(conda_dir);
352353
if let Some(mamba_mgr) = self.mamba_managers.get_or_insert_with(conda_dir.clone(), || {
353354
get_mamba_manager(conda_dir)
354355
}) {
355-
reporter.report_manager(&mamba_mgr.to_manager());
356+
if is_new_mamba {
357+
reporter.report_manager(&mamba_mgr.to_manager());
358+
}
356359
}
357360
} else {
358361
// We will still return the conda env even though we do not have the manager.

crates/pet-conda/src/manager.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,13 @@ fn get_mamba_executable(path: &Path) -> Option<PathBuf> {
4040
#[cfg(windows)]
4141
let relative_paths = vec![
4242
PathBuf::from("Scripts").join("mamba.exe"),
43+
PathBuf::from("Scripts").join("mamba.bat"),
4344
PathBuf::from("Scripts").join("micromamba.exe"),
45+
PathBuf::from("Scripts").join("micromamba.bat"),
4446
PathBuf::from("bin").join("mamba.exe"),
47+
PathBuf::from("bin").join("mamba.bat"),
4548
PathBuf::from("bin").join("micromamba.exe"),
49+
PathBuf::from("bin").join("micromamba.bat"),
4650
];
4751
#[cfg(unix)]
4852
let relative_paths = vec![
@@ -75,7 +79,7 @@ fn get_conda_bin_names() -> Vec<&'static str> {
7579
/// Specifically returns the file names that are valid for 'mamba'/'micromamba' on windows
7680
#[cfg(windows)]
7781
fn get_mamba_bin_names() -> Vec<&'static str> {
78-
vec!["mamba.exe", "micromamba.exe"]
82+
vec!["mamba.exe", "mamba.bat", "micromamba.exe", "micromamba.bat"]
7983
}
8084

8185
/// Specifically returns the file names that are valid for 'mamba'/'micromamba' on linux/Mac

crates/pet-conda/src/telemetry.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@ use pet_core::{
1111
};
1212

1313
use crate::{
14-
conda_info::CondaInfo, conda_rc::Condarc, env_variables::EnvVariables,
15-
environments::get_conda_environment_info, manager::CondaManager, utils::is_conda_install,
14+
conda_info::CondaInfo,
15+
conda_rc::Condarc,
16+
env_variables::EnvVariables,
17+
environments::get_conda_environment_info,
18+
manager::{is_mamba_executable, CondaManager},
19+
utils::is_conda_install,
1620
};
1721

1822
pub fn report_missing_envs(
@@ -221,11 +225,12 @@ fn log_and_find_missing_envs(
221225
.collect::<Vec<_>>();
222226

223227
// Oh oh, we have new envs, lets see what they are.
224-
let manager = CondaManager::from_info(
225-
&conda_info.executable,
226-
conda_info,
227-
pet_core::manager::EnvManagerType::Conda,
228-
)?;
228+
let manager_type = if is_mamba_executable(&conda_info.executable) {
229+
pet_core::manager::EnvManagerType::Mamba
230+
} else {
231+
pet_core::manager::EnvManagerType::Conda
232+
};
233+
let manager = CondaManager::from_info(&conda_info.executable, conda_info, manager_type)?;
229234
for path in missing_envs
230235
.clone()
231236
.iter()

crates/pet-conda/tests/ci_test.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,84 @@ fn detect_new_conda_env_created_with_p_flag_with_python() {
334334
assert_eq!(env.manager, Some(manager.clone()));
335335
}
336336

337+
#[cfg(unix)]
338+
#[cfg_attr(feature = "ci", test)]
339+
#[allow(dead_code)]
340+
// Install mamba into the conda base env and verify both Conda and Mamba managers are reported
341+
fn detect_mamba_manager_alongside_conda() {
342+
use std::sync::Arc;
343+
344+
use pet_conda::Conda;
345+
use pet_core::{
346+
manager::EnvManagerType, os_environment::EnvironmentApi,
347+
python_environment::PythonEnvironmentKind, Locator,
348+
};
349+
use pet_reporter::{cache::CacheReporter, collect};
350+
351+
setup();
352+
353+
// Install mamba into the base conda environment
354+
let output = std::process::Command::new(get_conda_exe())
355+
.args(["install", "-n", "base", "mamba", "-c", "conda-forge", "-y"])
356+
.output()
357+
.expect("Failed to install mamba");
358+
println!(
359+
"Mamba install stdout: {}",
360+
String::from_utf8_lossy(&output.stdout)
361+
);
362+
println!(
363+
"Mamba install stderr: {}",
364+
String::from_utf8_lossy(&output.stderr)
365+
);
366+
assert!(
367+
output.status.success(),
368+
"Failed to install mamba into conda base env"
369+
);
370+
371+
let env = EnvironmentApi::new();
372+
let reporter = Arc::new(collect::create_reporter());
373+
let conda = Conda::from(&env);
374+
conda.find(&CacheReporter::new(reporter.clone()));
375+
376+
let managers = reporter.managers.lock().unwrap().clone();
377+
378+
let info = get_conda_info();
379+
let conda_dir = PathBuf::from(info.conda_prefix.clone());
380+
381+
// Verify we have a Conda manager
382+
let conda_manager = managers
383+
.iter()
384+
.find(|m| m.tool == EnvManagerType::Conda)
385+
.unwrap_or_else(|| panic!("Conda manager not found in managers: {managers:?}"));
386+
assert_eq!(
387+
conda_manager.executable,
388+
conda_dir.join("bin").join("conda")
389+
);
390+
391+
// Verify we have a Mamba manager
392+
let mamba_manager = managers
393+
.iter()
394+
.find(|m| m.tool == EnvManagerType::Mamba)
395+
.unwrap_or_else(|| panic!("Mamba manager not found in managers: {managers:?}"));
396+
assert_eq!(
397+
mamba_manager.executable,
398+
conda_dir.join("bin").join("mamba")
399+
);
400+
401+
// Environments should still be reported as Conda kind
402+
let environments = reporter.environments.lock().unwrap().clone();
403+
let base_env = environments
404+
.iter()
405+
.find(|e| e.name == Some("base".into()))
406+
.unwrap_or_else(|| panic!("Base env not found in environments: {environments:?}"));
407+
assert_eq!(base_env.kind, Some(PythonEnvironmentKind::Conda));
408+
// The environment manager should be Conda (not Mamba)
409+
assert_eq!(
410+
base_env.manager.as_ref().map(|m| m.tool),
411+
Some(EnvManagerType::Conda)
412+
);
413+
}
414+
337415
#[derive(Deserialize)]
338416
struct CondaInfo {
339417
conda_version: String,

crates/pet-conda/tests/manager_test.rs

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,155 @@ fn does_not_find_conda_binary_when_not_on_path() {
131131

132132
assert!(conda_binary.is_none());
133133
}
134+
135+
// ==================== Mamba/Micromamba Tests ====================
136+
137+
/// Test is_mamba_executable correctly identifies mamba binaries.
138+
#[test]
139+
fn is_mamba_executable_identifies_mamba_binaries() {
140+
use pet_conda::manager::is_mamba_executable;
141+
use std::path::Path;
142+
143+
assert!(is_mamba_executable(Path::new("/usr/bin/mamba")));
144+
assert!(is_mamba_executable(Path::new("/usr/bin/micromamba")));
145+
assert!(is_mamba_executable(Path::new("C:\\Scripts\\mamba.exe")));
146+
assert!(is_mamba_executable(Path::new(
147+
"C:\\Scripts\\micromamba.exe"
148+
)));
149+
assert!(is_mamba_executable(Path::new("/opt/miniforge3/bin/mamba")));
150+
151+
// Should NOT match conda or python
152+
assert!(!is_mamba_executable(Path::new("/usr/bin/conda")));
153+
assert!(!is_mamba_executable(Path::new("/usr/bin/python")));
154+
assert!(!is_mamba_executable(Path::new("C:\\Scripts\\conda.exe")));
155+
}
156+
157+
/// Test that get_mamba_manager finds a mamba manager from a conda install with mamba.
158+
#[cfg(unix)]
159+
#[test]
160+
fn finds_mamba_manager_from_miniforge_root() {
161+
use common::resolve_test_path;
162+
use pet_conda::manager::CondaManager;
163+
use pet_core::manager::EnvManagerType;
164+
165+
let path = resolve_test_path(&["unix", "miniforge3-mamba"]);
166+
let manager = CondaManager::from(&path).unwrap();
167+
168+
// Should find conda first (conda takes precedence)
169+
assert_eq!(manager.executable, path.join("bin").join("conda"));
170+
assert_eq!(manager.version, Some("24.1.0".into()));
171+
assert_eq!(manager.manager_type, EnvManagerType::Conda);
172+
}
173+
174+
/// Test that CondaManager::from falls back to mamba when only micromamba is available.
175+
#[cfg(unix)]
176+
#[test]
177+
fn finds_mamba_manager_when_only_micromamba_available() {
178+
use common::resolve_test_path;
179+
use pet_conda::manager::CondaManager;
180+
use pet_core::manager::EnvManagerType;
181+
182+
let path = resolve_test_path(&["unix", "micromamba-only"]);
183+
let manager = CondaManager::from(&path).unwrap();
184+
185+
// No conda binary, should fall back to micromamba
186+
assert_eq!(manager.executable, path.join("bin").join("micromamba"));
187+
assert_eq!(manager.version, None); // version is unknown for mamba
188+
assert_eq!(manager.manager_type, EnvManagerType::Mamba);
189+
}
190+
191+
/// Test that find_mamba_binary finds mamba from the PATH environment variable.
192+
#[cfg(unix)]
193+
#[test]
194+
fn finds_mamba_binary_from_path() {
195+
use common::{create_test_environment, resolve_test_path};
196+
use pet_conda::env_variables::EnvVariables;
197+
use pet_conda::manager::find_mamba_binary;
198+
use std::collections::HashMap;
199+
200+
let miniforge_bin = resolve_test_path(&["unix", "miniforge3-mamba", "bin"]);
201+
let path_value = miniforge_bin.to_string_lossy().to_string();
202+
203+
let mut vars = HashMap::new();
204+
vars.insert("PATH".to_string(), path_value);
205+
206+
let env = create_test_environment(vars, None, vec![], None);
207+
let env_vars = EnvVariables::from(&env);
208+
209+
let mamba_binary = find_mamba_binary(&env_vars);
210+
211+
assert!(mamba_binary.is_some());
212+
assert_eq!(
213+
mamba_binary.unwrap(),
214+
resolve_test_path(&["unix", "miniforge3-mamba", "bin", "mamba"])
215+
);
216+
}
217+
218+
/// Test that find_mamba_binary finds micromamba when mamba is not available.
219+
#[cfg(unix)]
220+
#[test]
221+
fn finds_micromamba_binary_from_path() {
222+
use common::{create_test_environment, resolve_test_path};
223+
use pet_conda::env_variables::EnvVariables;
224+
use pet_conda::manager::find_mamba_binary;
225+
use std::collections::HashMap;
226+
227+
let micromamba_bin = resolve_test_path(&["unix", "micromamba-only", "bin"]);
228+
let path_value = micromamba_bin.to_string_lossy().to_string();
229+
230+
let mut vars = HashMap::new();
231+
vars.insert("PATH".to_string(), path_value);
232+
233+
let env = create_test_environment(vars, None, vec![], None);
234+
let env_vars = EnvVariables::from(&env);
235+
236+
let mamba_binary = find_mamba_binary(&env_vars);
237+
238+
assert!(mamba_binary.is_some());
239+
assert_eq!(
240+
mamba_binary.unwrap(),
241+
resolve_test_path(&["unix", "micromamba-only", "bin", "micromamba"])
242+
);
243+
}
244+
245+
/// Test that find_mamba_binary returns None when mamba is not on PATH.
246+
#[cfg(unix)]
247+
#[test]
248+
fn does_not_find_mamba_binary_when_not_on_path() {
249+
use common::{create_test_environment, resolve_test_path};
250+
use pet_conda::env_variables::EnvVariables;
251+
use pet_conda::manager::find_mamba_binary;
252+
use std::collections::HashMap;
253+
254+
// Use the anaconda path which has conda but no mamba
255+
let anaconda_bin = resolve_test_path(&["unix", "anaconda3-2023.03", "bin"]);
256+
let path_value = anaconda_bin.to_string_lossy().to_string();
257+
258+
let mut vars = HashMap::new();
259+
vars.insert("PATH".to_string(), path_value);
260+
261+
let env = create_test_environment(vars, None, vec![], None);
262+
let env_vars = EnvVariables::from(&env);
263+
264+
let mamba_binary = find_mamba_binary(&env_vars);
265+
266+
assert!(mamba_binary.is_none());
267+
}
268+
269+
/// Test that CondaManager::from finds mamba manager from a child env
270+
/// in a micromamba-only installation (no conda binary).
271+
#[cfg(unix)]
272+
#[test]
273+
fn finds_mamba_manager_from_child_env_in_miniforge() {
274+
use common::resolve_test_path;
275+
use pet_conda::manager::CondaManager;
276+
use pet_core::manager::EnvManagerType;
277+
278+
let path = resolve_test_path(&["unix", "miniforge3-mamba", "envs", "myenv"]);
279+
let manager = CondaManager::from(&path).unwrap();
280+
281+
// Should find the conda manager from the parent install dir
282+
let conda_dir = resolve_test_path(&["unix", "miniforge3-mamba"]);
283+
assert_eq!(manager.executable, conda_dir.join("bin").join("conda"));
284+
assert_eq!(manager.manager_type, EnvManagerType::Conda);
285+
}

crates/pet-conda/tests/unix/micromamba-only/bin/micromamba

Whitespace-only changes.

crates/pet-conda/tests/unix/micromamba-only/bin/python

Whitespace-only changes.

crates/pet-conda/tests/unix/micromamba-only/bin/python3

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
==> 2024-01-01 00:00:00 <==
2+
# cmd: micromamba install python=3.11.0
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"build": "h1234567_0",
3+
"name": "python",
4+
"version": "3.11.0"
5+
}

0 commit comments

Comments
 (0)