mock system interactions to fix flaky environment tests (#129)
Some checks failed
release / linux (map[runner:ubuntu-22.04 target:aarch64]) (push) Failing after 4s
release / linux (map[runner:ubuntu-22.04 target:ppc64le]) (push) Failing after 3s
release / linux (map[runner:ubuntu-22.04 target:s390x]) (push) Failing after 3s
release / linux (map[runner:ubuntu-22.04 target:x86]) (push) Failing after 3s
release / linux (map[runner:ubuntu-22.04 target:armv7]) (push) Failing after 3s
release / linux (map[runner:ubuntu-22.04 target:x86_64]) (push) Failing after 3s
release / musllinux (map[runner:ubuntu-22.04 target:aarch64]) (push) Failing after 3s
release / musllinux (map[runner:ubuntu-22.04 target:armv7]) (push) Failing after 3s
release / musllinux (map[runner:ubuntu-22.04 target:x86]) (push) Failing after 2s
release / musllinux (map[runner:ubuntu-22.04 target:x86_64]) (push) Failing after 3s
release / test (push) Has been skipped
lint / pre-commit (push) Has been cancelled
release / windows (map[runner:windows-latest target:x64]) (push) Has been cancelled
release / windows (map[runner:windows-latest target:x86]) (push) Has been cancelled
release / macos (map[runner:macos-13 target:x86_64]) (push) Has been cancelled
release / macos (map[runner:macos-14 target:aarch64]) (push) Has been cancelled
release / sdist (push) Has been cancelled
release / release (push) Has been cancelled

This commit is contained in:
Josh Thomas 2025-05-01 00:28:04 -05:00 committed by GitHub
parent 95a68e5f3a
commit 0d816ea0dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 303 additions and 136 deletions

View file

@ -1,12 +1,11 @@
mod system;
mod templatetags;
pub use templatetags::TemplateTags;
use pyo3::prelude::*;
use std::env;
use std::fmt;
use std::path::{Path, PathBuf};
use which::which;
#[derive(Debug)]
pub struct DjangoProject {
@ -92,19 +91,16 @@ impl PythonEnvironment {
fn new(project_path: &Path, venv_path: Option<&str>) -> Option<Self> {
if let Some(path) = venv_path {
let prefix = PathBuf::from(path);
// If an explicit path is provided and it's a valid venv, use it immediately.
if let Some(env) = Self::from_venv_prefix(&prefix) {
return Some(env);
}
// Explicit path was provided but was invalid. Continue searching.
// Invalid explicit path, continue searching...
}
if let Ok(virtual_env) = env::var("VIRTUAL_ENV") {
if !virtual_env.is_empty() {
let prefix = PathBuf::from(virtual_env);
if let Some(env) = Self::from_venv_prefix(&prefix) {
return Some(env);
}
if let Ok(virtual_env) = system::env_var("VIRTUAL_ENV") {
let prefix = PathBuf::from(virtual_env);
if let Some(env) = Self::from_venv_prefix(&prefix) {
return Some(env);
}
}
@ -126,7 +122,6 @@ impl PythonEnvironment {
#[cfg(windows)]
let python_path = prefix.join("Scripts").join("python.exe");
// Check if the *prefix* and the *binary* exist.
if !prefix.is_dir() || !python_path.exists() {
return None;
}
@ -137,10 +132,9 @@ impl PythonEnvironment {
let bin_dir = prefix.join("Scripts");
let mut sys_path = Vec::new();
sys_path.push(bin_dir); // Add bin/ or Scripts/
sys_path.push(bin_dir);
if let Some(site_packages) = Self::find_site_packages(prefix) {
// Check existence inside the if let, as find_site_packages might return a path that doesn't exist
if site_packages.is_dir() {
sys_path.push(site_packages);
}
@ -167,12 +161,10 @@ impl PythonEnvironment {
}
fn from_system_python() -> Option<Self> {
let python_path = match which("python") {
let python_path = match system::find_executable("python") {
Ok(p) => p,
Err(_) => return None,
};
// which() might return a path inside a bin/Scripts dir, or directly the executable
// We need the prefix, which is usually two levels up from the executable in standard layouts
let bin_dir = python_path.parent()?;
let prefix = bin_dir.parent()?;
@ -194,7 +186,6 @@ impl PythonEnvironment {
#[cfg(unix)]
fn find_site_packages(prefix: &Path) -> Option<PathBuf> {
// Look for lib/pythonX.Y/site-packages
let lib_dir = prefix.join("lib");
if !lib_dir.is_dir() {
return None;
@ -203,8 +194,8 @@ impl PythonEnvironment {
.ok()?
.filter_map(Result::ok)
.find(|e| {
e.file_type().is_ok_and(|ft| ft.is_dir()) && // Ensure it's a directory
e.file_name().to_string_lossy().starts_with("python")
e.file_type().is_ok_and(|ft| ft.is_dir())
&& e.file_name().to_string_lossy().starts_with("python")
})
.map(|e| e.path().join("site-packages"))
}
@ -231,10 +222,14 @@ impl fmt::Display for PythonEnvironment {
mod tests {
use super::*;
use std::fs;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use tempfile::tempdir;
mod env_discovery {
use super::system::mock::{self as sys_mock, MockGuard};
use super::*;
use which::Error as WhichError;
fn create_mock_venv(dir: &Path, version: Option<&str>) -> PathBuf {
let prefix = dir.to_path_buf();
@ -243,7 +238,7 @@ mod tests {
{
let bin_dir = prefix.join("bin");
fs::create_dir_all(&bin_dir).unwrap();
fs::write(bin_dir.join("python"), "").unwrap(); // Create dummy executable
fs::write(bin_dir.join("python"), "").unwrap();
let lib_dir = prefix.join("lib");
fs::create_dir_all(&lib_dir).unwrap();
let py_version_dir = lib_dir.join(version.unwrap_or("python3.9"));
@ -254,7 +249,7 @@ mod tests {
{
let bin_dir = prefix.join("Scripts");
fs::create_dir_all(&bin_dir).unwrap();
fs::write(bin_dir.join("python.exe"), "").unwrap(); // Create dummy executable
fs::write(bin_dir.join("python.exe"), "").unwrap();
let lib_dir = prefix.join("Lib");
fs::create_dir_all(&lib_dir).unwrap();
fs::create_dir_all(lib_dir.join("site-packages")).unwrap();
@ -263,32 +258,6 @@ mod tests {
prefix
}
struct VirtualEnvGuard<'a> {
key: &'a str,
original_value: Option<String>,
}
impl<'a> VirtualEnvGuard<'a> {
fn set(key: &'a str, value: &str) -> Self {
let original_value = env::var(key).ok();
env::set_var(key, value);
Self {
key,
original_value,
}
}
}
impl Drop for VirtualEnvGuard<'_> {
fn drop(&mut self) {
if let Some(ref val) = self.original_value {
env::set_var(self.key, val);
} else {
env::remove_var(self.key);
}
}
}
#[test]
fn test_explicit_venv_path_found() {
let project_dir = tempdir().unwrap();
@ -324,11 +293,9 @@ mod tests {
let project_dir = tempdir().unwrap();
let project_venv_prefix = create_mock_venv(&project_dir.path().join(".venv"), None);
// Set VIRTUAL_ENV to something known to be invalid, rather than clearing.
// This prevents the test runner's VIRTUAL_ENV (e.g., from Nox) from interfering.
let invalid_virtual_env_path = project_dir.path().join("non_existent_virtual_env");
let _guard =
VirtualEnvGuard::set("VIRTUAL_ENV", invalid_virtual_env_path.to_str().unwrap());
let _guard = MockGuard;
// Ensure VIRTUAL_ENV is not set (returns VarError::NotPresent)
sys_mock::remove_env_var("VIRTUAL_ENV");
// Provide an invalid explicit path
let invalid_path = project_dir.path().join("non_existent_venv");
@ -346,7 +313,9 @@ mod tests {
let venv_dir = tempdir().unwrap();
let venv_prefix = create_mock_venv(venv_dir.path(), None);
let _guard = VirtualEnvGuard::set("VIRTUAL_ENV", venv_prefix.to_str().unwrap());
let _guard = MockGuard;
// Mock VIRTUAL_ENV to point to the mock venv
sys_mock::set_env_var("VIRTUAL_ENV", venv_prefix.to_str().unwrap().to_string());
let env = PythonEnvironment::new(project_dir.path(), None)
.expect("Should find environment via VIRTUAL_ENV");
@ -363,18 +332,20 @@ mod tests {
fn test_explicit_path_overrides_virtual_env() {
let project_dir = tempdir().unwrap();
let venv1_dir = tempdir().unwrap();
let venv1_prefix = create_mock_venv(venv1_dir.path(), None); // Set by VIRTUAL_ENV
let venv1_prefix = create_mock_venv(venv1_dir.path(), None); // Mocked by VIRTUAL_ENV
let venv2_dir = tempdir().unwrap();
let venv2_prefix = create_mock_venv(venv2_dir.path(), None); // Set by explicit path
let venv2_prefix = create_mock_venv(venv2_dir.path(), None); // Provided explicitly
let _guard = VirtualEnvGuard::set("VIRTUAL_ENV", venv1_prefix.to_str().unwrap());
let _guard = MockGuard;
// Mock VIRTUAL_ENV to point to venv1
sys_mock::set_env_var("VIRTUAL_ENV", venv1_prefix.to_str().unwrap().to_string());
let env = PythonEnvironment::new(
project_dir.path(),
Some(venv2_prefix.to_str().unwrap()), // Explicit path
)
.expect("Should find environment via explicit path");
// Call with explicit path to venv2
let env =
PythonEnvironment::new(project_dir.path(), Some(venv2_prefix.to_str().unwrap()))
.expect("Should find environment via explicit path");
// Explicit path (venv2) should take precedence
assert_eq!(
env.sys_prefix, venv2_prefix,
"Explicit path should take precedence"
@ -386,10 +357,9 @@ mod tests {
let project_dir = tempdir().unwrap();
let venv_prefix = create_mock_venv(&project_dir.path().join(".venv"), None);
// Set VIRTUAL_ENV to something known to be invalid to ensure it's ignored.
let invalid_virtual_env_path = project_dir.path().join("non_existent_venv_proj_found");
let _guard =
VirtualEnvGuard::set("VIRTUAL_ENV", invalid_virtual_env_path.to_str().unwrap());
let _guard = MockGuard;
// Ensure VIRTUAL_ENV is not set
sys_mock::remove_env_var("VIRTUAL_ENV");
let env = PythonEnvironment::new(project_dir.path(), None)
.expect("Should find environment in project .venv");
@ -400,48 +370,88 @@ mod tests {
#[test]
fn test_project_venv_priority() {
let project_dir = tempdir().unwrap();
// Create multiple potential venvs
let dot_venv_prefix = create_mock_venv(&project_dir.path().join(".venv"), None);
let _venv_prefix = create_mock_venv(&project_dir.path().join("venv"), None); // Should be ignored if .venv found first
let _venv_prefix = create_mock_venv(&project_dir.path().join("venv"), None);
// Set VIRTUAL_ENV to something known to be invalid to ensure it's ignored.
let invalid_virtual_env_path = project_dir.path().join("non_existent_venv_priority");
let _guard =
VirtualEnvGuard::set("VIRTUAL_ENV", invalid_virtual_env_path.to_str().unwrap());
let _guard = MockGuard;
// Ensure VIRTUAL_ENV is not set
sys_mock::remove_env_var("VIRTUAL_ENV");
let env =
PythonEnvironment::new(project_dir.path(), None).expect("Should find environment");
// Asserts it finds .venv because it's checked first in the loop
// Should find .venv because it's checked first in the loop
assert_eq!(env.sys_prefix, dot_venv_prefix);
}
#[test]
#[ignore = "Relies on system python being available and having standard layout"]
fn test_system_python_fallback() {
let project_dir = tempdir().unwrap();
// Set VIRTUAL_ENV to something known to be invalid to ensure it's ignored.
let invalid_virtual_env_path =
project_dir.path().join("non_existent_venv_sys_fallback");
let _guard =
VirtualEnvGuard::set("VIRTUAL_ENV", invalid_virtual_env_path.to_str().unwrap());
// We don't create any venvs in project_dir
let _guard = MockGuard;
// Ensure VIRTUAL_ENV is not set
sys_mock::remove_env_var("VIRTUAL_ENV");
let mock_sys_python_dir = tempdir().unwrap();
let mock_sys_python_prefix = mock_sys_python_dir.path();
#[cfg(unix)]
let (bin_subdir, python_exe, site_packages_rel_path) = (
"bin",
"python",
Path::new("lib").join("python3.9").join("site-packages"),
);
#[cfg(windows)]
let (bin_subdir, python_exe, site_packages_rel_path) = (
"Scripts",
"python.exe",
Path::new("Lib").join("site-packages"),
);
let bin_dir = mock_sys_python_prefix.join(bin_subdir);
fs::create_dir_all(&bin_dir).unwrap();
let python_path = bin_dir.join(python_exe);
fs::write(&python_path, "").unwrap();
#[cfg(unix)]
{
let mut perms = fs::metadata(&python_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&python_path, perms).unwrap();
}
let site_packages_path = mock_sys_python_prefix.join(site_packages_rel_path);
fs::create_dir_all(&site_packages_path).unwrap();
sys_mock::set_exec_path("python", python_path.clone());
// This test assumes `which python` works and points to a standard layout
let system_env = PythonEnvironment::new(project_dir.path(), None);
// Assert it found the mock system python via the mocked finder
assert!(
system_env.is_some(),
"Should fall back to system python if available"
"Should fall back to the mock system python"
);
if let Some(env) = system_env {
// Basic checks - exact paths depend heavily on the test environment
assert!(env.python_path.exists());
assert!(env.sys_prefix.exists());
assert!(!env.sys_path.is_empty());
assert!(env.sys_path[0].exists()); // Should contain the bin/Scripts dir
assert_eq!(
env.python_path, python_path,
"Python path should match mock"
);
assert_eq!(
env.sys_prefix, mock_sys_python_prefix,
"Sys prefix should match mock prefix"
);
assert!(
env.sys_path.contains(&bin_dir),
"Sys path should contain mock bin dir"
);
assert!(
env.sys_path.contains(&site_packages_path),
"Sys path should contain mock site-packages"
);
} else {
panic!("Expected to find environment, but got None");
}
}
@ -449,39 +459,19 @@ mod tests {
fn test_no_python_found() {
let project_dir = tempdir().unwrap();
// Ensure no explicit path, no project venvs, and set VIRTUAL_ENV to invalid.
let invalid_virtual_env_path = project_dir.path().join("non_existent_venv_no_python");
let _guard =
VirtualEnvGuard::set("VIRTUAL_ENV", invalid_virtual_env_path.to_str().unwrap());
let _guard = MockGuard; // Setup guard to clear mocks
// To *ensure* system fallback fails, we'd need to manipulate PATH,
// which is tricky and platform-dependent. Instead, we test the scenario
// where `from_system_python` *would* be called but returns None.
// We can simulate this by ensuring `which("python")` fails.
// For this unit test, let's assume a scenario where all checks fail.
// A more direct test would mock `which`, but that adds complexity.
// Ensure VIRTUAL_ENV is not set
sys_mock::remove_env_var("VIRTUAL_ENV");
// Let's simulate the *call* path assuming `from_system_python` returns None.
// We can't easily force `which` to fail here without PATH manipulation.
// So, this test mainly verifies that if all preceding steps fail,
// the result of `from_system_python` (which *could* be None) is returned.
// If system python *is* found, this test might incorrectly pass if not ignored.
// A better approach might be needed if strict testing of "None" is required.
// Ensure find_executable returns an error
sys_mock::set_exec_error("python", WhichError::CannotFindBinaryPath);
// For now, let's assume a setup where system python isn't found by `which`.
// This test is inherently flaky if system python *is* on the PATH.
// Consider ignoring it or using mocking for `which` in a real-world scenario.
// If system python IS found, this test doesn't truly test the "None" case.
// If system python IS NOT found, it tests the final `None` return.
let env = PythonEnvironment::new(project_dir.path(), None);
// This assertion depends on whether system python is actually found or not.
// assert!(env.is_none(), "Expected no environment to be found");
// Given the difficulty, let's skip asserting None directly unless we mock `which`.
println!(
"Test 'test_no_python_found' ran. Result depends on system state: {:?}",
env
assert!(
env.is_none(),
"Expected no environment to be found when all discovery methods fail"
);
}
@ -495,7 +485,6 @@ mod tests {
fs::write(bin_dir.join("python"), "").unwrap();
let lib_dir = prefix.join("lib");
fs::create_dir_all(&lib_dir).unwrap();
// Create two python version dirs, ensure it picks one
let py_version_dir1 = lib_dir.join("python3.8");
fs::create_dir_all(&py_version_dir1).unwrap();
fs::create_dir_all(py_version_dir1.join("site-packages")).unwrap();
@ -505,14 +494,11 @@ mod tests {
let env = PythonEnvironment::from_venv_prefix(prefix).unwrap();
// It should find *a* site-packages dir. The exact one depends on read_dir order.
let found_site_packages = env.sys_path.iter().any(|p| p.ends_with("site-packages"));
assert!(
found_site_packages,
"Should have found a site-packages directory"
);
// Ensure it contains the bin dir as well
assert!(env.sys_path.contains(&prefix.join("bin")));
}
@ -527,7 +513,7 @@ mod tests {
let lib_dir = prefix.join("Lib");
fs::create_dir_all(&lib_dir).unwrap();
let site_packages = lib_dir.join("site-packages");
fs::create_dir_all(&site_packages).unwrap(); // Create the actual dir
fs::create_dir_all(&site_packages).unwrap();
let env = PythonEnvironment::from_venv_prefix(prefix).unwrap();
@ -541,7 +527,6 @@ mod tests {
#[test]
fn test_from_venv_prefix_returns_none_if_dir_missing() {
let dir = tempdir().unwrap();
// Don't create the venv structure
let result = PythonEnvironment::from_venv_prefix(dir.path());
assert!(result.is_none());
}
@ -550,7 +535,6 @@ mod tests {
fn test_from_venv_prefix_returns_none_if_binary_missing() {
let dir = tempdir().unwrap();
let prefix = dir.path();
// Create prefix dir but not the binary
fs::create_dir_all(prefix).unwrap();
#[cfg(unix)]
@ -574,7 +558,6 @@ mod tests {
fn create_test_env(sys_paths: Vec<PathBuf>) -> PythonEnvironment {
PythonEnvironment {
// Dummy values for fields not directly used by activate
python_path: PathBuf::from("dummy/bin/python"),
sys_prefix: PathBuf::from("dummy"),
sys_path: sys_paths,
@ -605,8 +588,6 @@ mod tests {
initial_len + 2,
"Should have added 2 paths"
);
// Check that the *exact* paths were appended in the correct order
assert_eq!(
final_sys_path.get(initial_len).unwrap(),
path1.to_str().expect("Path 1 should be valid UTF-8")
@ -644,7 +625,6 @@ mod tests {
#[test]
fn test_activate_with_non_existent_paths() -> PyResult<()> {
let temp_dir = tempdir().unwrap();
// These paths do not actually exist on the filesystem
let path1 = temp_dir.path().join("non_existent_dir");
let path2 = temp_dir.path().join("another_missing/path");
@ -687,11 +667,9 @@ mod tests {
let valid_path = temp_dir.path().join("valid_dir");
fs::create_dir(&valid_path).unwrap();
// Create a PathBuf from invalid UTF-8 bytes
let invalid_bytes = b"invalid_\xff_utf8";
let os_str = OsStr::from_bytes(invalid_bytes);
let non_utf8_path = PathBuf::from(os_str);
// Sanity check: ensure this path *cannot* be converted to str
assert!(
non_utf8_path.to_str().is_none(),
"Path should not be convertible to UTF-8 str"
@ -708,7 +686,6 @@ mod tests {
test_env.activate(py)?;
let final_sys_path = get_sys_path(py)?;
// Should have added only the valid path
assert_eq!(
final_sys_path.len(),
initial_len + 1,
@ -719,7 +696,6 @@ mod tests {
valid_path.to_str().unwrap()
);
// Check that the invalid path string representation is NOT present
let invalid_path_lossy = non_utf8_path.to_string_lossy();
assert!(
!final_sys_path
@ -733,17 +709,14 @@ mod tests {
}
#[test]
#[cfg(windows)] // Test specific behavior for invalid UTF-16/WTF-8 on Windows
#[cfg(windows)]
fn test_activate_skips_non_utf8_paths_windows() -> PyResult<()> {
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
let temp_dir = tempdir().unwrap();
let valid_path = temp_dir.path().join("valid_dir");
// No need to create dir, just need the PathBuf
// Create an OsString from invalid UTF-16 (a lone surrogate)
// D800 is a high surrogate, not valid unless paired with a low surrogate.
let invalid_wide: Vec<u16> = vec![
'i' as u16, 'n' as u16, 'v' as u16, 'a' as u16, 'l' as u16, 'i' as u16, 'd' as u16,
'_' as u16, 0xD800, '_' as u16, 'w' as u16, 'i' as u16, 'd' as u16, 'e' as u16,
@ -751,7 +724,6 @@ mod tests {
let os_string = OsString::from_wide(&invalid_wide);
let non_utf8_path = PathBuf::from(os_string);
// Sanity check: ensure this path *cannot* be converted to a valid UTF-8 str
assert!(
non_utf8_path.to_str().is_none(),
"Path with lone surrogate should not be convertible to UTF-8 str"
@ -768,7 +740,6 @@ mod tests {
test_env.activate(py)?;
let final_sys_path = get_sys_path(py)?;
// Should have added only the valid path
assert_eq!(
final_sys_path.len(),
initial_len + 1,
@ -779,7 +750,6 @@ mod tests {
valid_path.to_str().unwrap()
);
// Check that the invalid path string representation is NOT present
let invalid_path_lossy = non_utf8_path.to_string_lossy();
assert!(
!final_sys_path

View file

@ -0,0 +1,196 @@
use std::env::VarError;
use std::path::PathBuf;
use which::Error as WhichError;
pub fn find_executable(name: &str) -> Result<PathBuf, WhichError> {
#[cfg(not(test))]
{
which::which(name)
}
#[cfg(test)]
{
mock::find_executable_mocked(name)
}
}
pub fn env_var(key: &str) -> Result<String, VarError> {
#[cfg(not(test))]
{
std::env::var(key)
}
#[cfg(test)]
{
mock::env_var_mocked(key)
}
}
#[cfg(test)]
pub mod mock {
use super::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::thread_local;
thread_local! {
static MOCK_EXEC_RESULTS: RefCell<HashMap<String, Result<PathBuf, WhichError>>> = RefCell::new(HashMap::new());
static MOCK_ENV_RESULTS: RefCell<HashMap<String, Result<String, VarError>>> = RefCell::new(HashMap::new());
}
pub(super) fn find_executable_mocked(name: &str) -> Result<PathBuf, WhichError> {
MOCK_EXEC_RESULTS.with(|mocks| {
mocks
.borrow()
.get(name)
.cloned()
.unwrap_or(Err(WhichError::CannotFindBinaryPath))
})
}
pub(super) fn env_var_mocked(key: &str) -> Result<String, VarError> {
MOCK_ENV_RESULTS.with(|mocks| {
mocks
.borrow()
.get(key)
.cloned()
.unwrap_or(Err(VarError::NotPresent))
})
}
// RAII guard to clear all mocks automatically after each test.
pub struct MockGuard;
impl Drop for MockGuard {
fn drop(&mut self) {
MOCK_EXEC_RESULTS.with(|mocks| mocks.borrow_mut().clear());
MOCK_ENV_RESULTS.with(|mocks| mocks.borrow_mut().clear());
}
}
pub fn set_exec_path(name: &str, path: PathBuf) {
MOCK_EXEC_RESULTS.with(|mocks| {
mocks.borrow_mut().insert(name.to_string(), Ok(path));
});
}
pub fn set_exec_error(name: &str, error: WhichError) {
MOCK_EXEC_RESULTS.with(|mocks| {
mocks.borrow_mut().insert(name.to_string(), Err(error));
});
}
pub fn set_env_var(key: &str, value: String) {
MOCK_ENV_RESULTS.with(|mocks| {
mocks.borrow_mut().insert(key.to_string(), Ok(value));
});
}
// Simulates VarError::NotPresent
pub fn remove_env_var(key: &str) {
MOCK_ENV_RESULTS.with(|mocks| {
mocks
.borrow_mut()
.insert(key.to_string(), Err(VarError::NotPresent));
});
}
}
#[cfg(test)]
mod tests {
use super::mock::{self as sys_mock, MockGuard};
use super::*;
use std::env::VarError;
use std::path::PathBuf;
use which::Error as WhichError;
#[test]
fn test_exec_mock_path_retrieval() {
let _guard = MockGuard;
let expected_path = PathBuf::from("/mock/path/to/python");
sys_mock::set_exec_path("python", expected_path.clone());
let result = find_executable("python");
assert_eq!(result.unwrap(), expected_path);
}
#[test]
fn test_exec_mock_error_retrieval() {
let _guard = MockGuard;
sys_mock::set_exec_error("cargo", WhichError::CannotFindBinaryPath);
let result = find_executable("cargo");
assert!(matches!(result, Err(WhichError::CannotFindBinaryPath)));
}
#[test]
fn test_exec_mock_default_error_if_unmocked() {
let _guard = MockGuard;
let result = find_executable("git"); // Not mocked
assert!(matches!(result, Err(WhichError::CannotFindBinaryPath)));
}
#[test]
fn test_env_mock_set_var_retrieval() {
let _guard = MockGuard;
sys_mock::set_env_var("MY_VAR", "my_value".to_string());
let result = env_var("MY_VAR");
assert_eq!(result.unwrap(), "my_value");
}
#[test]
fn test_env_mock_remove_var_retrieval() {
let _guard = MockGuard;
// Set it first, then remove it via mock
sys_mock::set_env_var("TEMP_VAR", "temp_value".to_string());
sys_mock::remove_env_var("TEMP_VAR");
let result = env_var("TEMP_VAR");
assert!(matches!(result, Err(VarError::NotPresent)));
}
#[test]
fn test_env_mock_default_error_if_unmocked() {
let _guard = MockGuard;
let result = env_var("UNMOCKED_VAR"); // Not mocked
assert!(matches!(result, Err(VarError::NotPresent)));
}
#[test]
fn test_mock_guard_clears_all_mocks() {
let expected_exec_path = PathBuf::from("/tmp/myprog");
let expected_env_val = "test_value".to_string();
{
let _guard = MockGuard;
sys_mock::set_exec_path("myprog", expected_exec_path.clone());
sys_mock::set_env_var("MY_TEST_ENV", expected_env_val.clone());
// Guard drops here, clearing both mocks
}
// Verify mocks were cleared
let _guard = MockGuard;
let result_exec = find_executable("myprog");
assert!(matches!(result_exec, Err(WhichError::CannotFindBinaryPath)));
let result_env = env_var("MY_TEST_ENV");
assert!(matches!(result_env, Err(VarError::NotPresent)));
}
#[test]
fn test_mocks_are_separate_between_tests() {
let _guard = MockGuard; // Ensures clean state
// Check state from previous tests (should be cleared)
let result_python = find_executable("python");
assert!(matches!(
result_python,
Err(WhichError::CannotFindBinaryPath)
));
let result_myvar = env_var("MY_VAR");
assert!(matches!(result_myvar, Err(VarError::NotPresent)));
// Set mocks specific to this test
let expected_path_node = PathBuf::from("/usr/bin/node");
sys_mock::set_exec_path("node", expected_path_node.clone());
sys_mock::set_env_var("NODE_ENV", "production".to_string());
let result_node = find_executable("node");
assert_eq!(result_node.unwrap(), expected_path_node);
let result_node_env = env_var("NODE_ENV");
assert_eq!(result_node_env.unwrap(), "production");
}
}