use std::sync::Mutex;
use tempfile::TempDir;
use crate::paths::SCOOP_HOME_ENV;
pub static ENV_LOCK: Mutex<()> = Mutex::new(());
pub fn with_no_scoop_home<F, T>(f: F) -> T
where
F: FnOnce() -> T,
{
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let backup = std::env::var(SCOOP_HOME_ENV).ok();
unsafe { std::env::remove_var(SCOOP_HOME_ENV) };
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
if let Some(val) = backup {
unsafe { std::env::set_var(SCOOP_HOME_ENV, val) };
}
match result {
Ok(val) => val,
Err(e) => std::panic::resume_unwind(e),
}
}
pub fn with_temp_scoop_home<F, T>(f: F) -> T
where
F: FnOnce(&TempDir) -> T,
{
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let temp_dir = TempDir::new().expect("Failed to create temp dir for SCOOP_HOME");
unsafe { std::env::set_var(SCOOP_HOME_ENV, temp_dir.path()) };
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&temp_dir)));
unsafe { std::env::remove_var(SCOOP_HOME_ENV) };
match result {
Ok(val) => val,
Err(e) => std::panic::resume_unwind(e),
}
}
pub fn create_mock_venv(temp_dir: &TempDir, name: &str, python_version: Option<&str>) {
use crate::core::Metadata;
use std::fs;
let venvs_dir = temp_dir.path().join("virtualenvs");
let venv_path = venvs_dir.join(name);
fs::create_dir_all(&venv_path).expect("Failed to create mock venv directory");
if let Some(version) = python_version {
let meta = Metadata::new(name.to_string(), version.to_string(), None);
let meta_path = venv_path.join(Metadata::FILE_NAME);
fs::write(meta_path, serde_json::to_string(&meta).unwrap())
.expect("Failed to write mock metadata");
}
}
#[macro_export]
macro_rules! assert_valid_env {
($name:expr) => {
assert!(
$crate::validate::is_valid_env_name($name),
"Expected '{}' to be a valid env name",
$name
);
};
}
#[macro_export]
macro_rules! assert_invalid_env {
($name:expr) => {
assert!(
!$crate::validate::is_valid_env_name($name),
"Expected '{}' to be an invalid env name",
$name
);
};
}
#[macro_export]
macro_rules! assert_valid_version {
($version:expr) => {
assert!(
$crate::validate::is_valid_python_version($version),
"Expected '{}' to be a valid Python version",
$version
);
};
}
#[macro_export]
macro_rules! assert_invalid_version {
($version:expr) => {
assert!(
!$crate::validate::is_valid_python_version($version),
"Expected '{}' to be an invalid Python version",
$version
);
};
}
#[macro_export]
macro_rules! assert_error_variant {
($err:expr, $variant:pat) => {
assert!(
matches!($err, $variant),
"Expected error variant {}, got {:?}",
stringify!($variant),
$err
);
};
}
const MIGRATE_ENV_VARS: &[&str] = &[
"PYENV_ROOT",
"PYENV_VIRTUALENV_INIT",
"WORKON_HOME",
"VIRTUALENVWRAPPER_HOOK_DIR",
"CONDA_PREFIX",
"CONDA_EXE",
];
pub static MIGRATE_ENV_LOCK: Mutex<()> = Mutex::new(());
pub fn with_isolated_migrate_env<F, T>(f: F) -> T
where
F: FnOnce() -> T,
{
use std::collections::HashMap;
let _guard = MIGRATE_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let temp_home = TempDir::new().expect("Failed to create temp HOME for migrate isolation");
let mut backup: HashMap<&str, Option<String>> = HashMap::new();
for var in MIGRATE_ENV_VARS {
backup.insert(var, std::env::var(var).ok());
unsafe { std::env::remove_var(var) };
}
backup.insert("HOME", std::env::var("HOME").ok());
unsafe { std::env::set_var("HOME", temp_home.path()) };
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
for (var, value) in backup {
unsafe {
match value {
Some(v) => std::env::set_var(var, v),
None => std::env::remove_var(var),
}
}
}
match result {
Ok(val) => val,
Err(e) => std::panic::resume_unwind(e),
}
}
pub fn create_mock_pyenv_env(pyenv_root: &std::path::Path, name: &str, python_version: &str) {
use std::fs;
let versions_dir = pyenv_root.join("versions");
let python_dir = versions_dir.join(python_version);
let envs_dir = python_dir.join("envs");
let env_dir = envs_dir.join(name);
let bin_dir = env_dir.join("bin");
fs::create_dir_all(&bin_dir).expect("Failed to create mock pyenv bin directory");
let pyvenv_cfg = format!(
"home = {}/versions/{}/bin\nversion = {}\n",
pyenv_root.display(),
python_version,
python_version
);
fs::write(env_dir.join("pyvenv.cfg"), pyvenv_cfg).expect("Failed to write pyvenv.cfg");
fs::write(bin_dir.join("python"), "").expect("Failed to write mock python binary");
}
pub fn create_corrupted_pyenv_env(pyenv_root: &std::path::Path, name: &str, python_version: &str) {
use std::fs;
let versions_dir = pyenv_root.join("versions");
let python_dir = versions_dir.join(python_version);
let envs_dir = python_dir.join("envs");
let env_dir = envs_dir.join(name);
fs::create_dir_all(&env_dir).expect("Failed to create mock pyenv directory");
let pyvenv_cfg = format!("version = {}\n", python_version);
fs::write(env_dir.join("pyvenv.cfg"), pyvenv_cfg).expect("Failed to write pyvenv.cfg");
}
pub fn with_full_migrate_env<F, T>(f: F) -> T
where
F: FnOnce(&TempDir, &TempDir) -> T,
{
use std::collections::HashMap;
let _env_guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _migrate_guard = MIGRATE_ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let scoop_home = TempDir::new().expect("Failed to create temp SCOOP_HOME");
let pyenv_root = TempDir::new().expect("Failed to create temp PYENV_ROOT");
std::fs::create_dir_all(scoop_home.path().join("virtualenvs"))
.expect("Failed to create virtualenvs dir");
let mut backup: HashMap<&str, Option<String>> = HashMap::new();
for var in MIGRATE_ENV_VARS {
backup.insert(var, std::env::var(var).ok());
unsafe { std::env::remove_var(var) };
}
backup.insert("SCOOP_HOME", std::env::var("SCOOP_HOME").ok());
unsafe {
std::env::set_var(SCOOP_HOME_ENV, scoop_home.path());
std::env::set_var("PYENV_ROOT", pyenv_root.path());
}
let result =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(&scoop_home, &pyenv_root)));
for (var, value) in backup {
unsafe {
match value {
Some(v) => std::env::set_var(var, v),
None => std::env::remove_var(var),
}
}
}
match result {
Ok(val) => val,
Err(e) => std::panic::resume_unwind(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::path::PathBuf;
#[test]
#[serial]
fn test_with_temp_scoop_home_sets_env() {
with_temp_scoop_home(|temp_dir| {
let scoop_home = std::env::var(SCOOP_HOME_ENV).unwrap();
assert_eq!(PathBuf::from(scoop_home), temp_dir.path());
});
}
#[test]
#[serial]
fn test_with_temp_scoop_home_cleans_up() {
with_temp_scoop_home(|_| {
});
assert!(std::env::var(SCOOP_HOME_ENV).is_err());
}
#[test]
#[serial]
fn test_create_mock_venv_creates_directory() {
with_temp_scoop_home(|temp_dir| {
create_mock_venv(temp_dir, "testenv", None);
let venv_path = temp_dir.path().join("virtualenvs").join("testenv");
assert!(venv_path.exists());
assert!(venv_path.is_dir());
});
}
#[test]
#[serial]
fn test_create_mock_venv_with_metadata() {
with_temp_scoop_home(|temp_dir| {
create_mock_venv(temp_dir, "withversion", Some("3.12"));
let meta_path = temp_dir
.path()
.join("virtualenvs")
.join("withversion")
.join(".scoop-metadata.json");
assert!(meta_path.exists());
let content = std::fs::read_to_string(meta_path).unwrap();
assert!(content.contains("\"python_version\""));
assert!(content.contains("\"3.12\""));
});
}
#[test]
#[serial]
fn test_multiple_mock_venvs_sequential() {
with_temp_scoop_home(|temp_dir| {
for i in 0..5 {
let name = format!("env{}", i);
create_mock_venv(temp_dir, &name, Some("3.12"));
}
for i in 0..5 {
let name = format!("env{}", i);
let path = temp_dir.path().join("virtualenvs").join(&name);
assert!(path.exists(), "env{} should exist", i);
}
});
}
#[test]
fn test_env_lock_prevents_concurrent_modification() {
use std::sync::Arc;
use std::thread;
let counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let mut handles = vec![];
for _ in 0..4 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let current = counter.load(std::sync::atomic::Ordering::SeqCst);
thread::yield_now();
counter.store(current + 1, std::sync::atomic::Ordering::SeqCst);
});
handles.push(handle);
}
for handle in handles {
handle.join().expect("Thread should not panic");
}
assert_eq!(
counter.load(std::sync::atomic::Ordering::SeqCst),
4,
"All threads should have incremented"
);
}
#[test]
fn test_metadata_serialization_is_deterministic() {
use crate::core::Metadata;
let mut jsons = Vec::new();
for _ in 0..3 {
let meta = Metadata::new(
"test".to_string(),
"3.12".to_string(),
Some("1.0".to_string()),
);
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("\"name\":\"test\""));
assert!(json.contains("\"python_version\":\"3.12\""));
jsons.push(json);
}
for json in &jsons {
assert!(json.contains("created_at"));
assert!(json.contains("created_by"));
}
}
#[test]
#[serial]
fn test_with_isolated_migrate_env_unsets_vars() {
with_isolated_migrate_env(|| {
assert!(std::env::var("PYENV_ROOT").is_err());
assert!(std::env::var("WORKON_HOME").is_err());
assert!(std::env::var("CONDA_PREFIX").is_err());
});
}
#[test]
#[serial]
fn test_with_isolated_migrate_env_restores_vars() {
unsafe { std::env::set_var("PYENV_ROOT", "/original/path") };
with_isolated_migrate_env(|| {
assert!(std::env::var("PYENV_ROOT").is_err());
});
assert_eq!(std::env::var("PYENV_ROOT").unwrap(), "/original/path");
unsafe { std::env::remove_var("PYENV_ROOT") };
}
#[test]
fn test_assert_valid_env_macro() {
assert_valid_env!("myenv");
assert_valid_env!("my-project");
assert_valid_env!("test_env");
}
#[test]
fn test_assert_invalid_env_macro() {
assert_invalid_env!("123");
assert_invalid_env!("activate");
assert_invalid_env!("");
}
#[test]
fn test_assert_valid_version_macro() {
assert_valid_version!("3");
assert_valid_version!("3.12");
assert_valid_version!("3.12.0");
}
#[test]
fn test_assert_invalid_version_macro() {
assert_invalid_version!("");
assert_invalid_version!("abc");
assert_invalid_version!("v3.12");
}
#[test]
fn test_assert_error_variant_macro() {
use crate::error::ScoopError;
let err = ScoopError::VirtualenvNotFound {
name: "test".to_string(),
};
assert_error_variant!(err, ScoopError::VirtualenvNotFound { .. });
let err = ScoopError::HomeNotFound;
assert_error_variant!(err, ScoopError::HomeNotFound);
}
}