mirror of
https://github.com/joshuadavidthomas/django-language-server.git
synced 2025-07-09 21:54:59 +00:00
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
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:
parent
95a68e5f3a
commit
0d816ea0dc
3 changed files with 303 additions and 136 deletions
|
@ -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
|
||||
|
|
196
crates/djls-project/src/system.rs
Normal file
196
crates/djls-project/src/system.rs
Normal 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");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue