Skip to content

Commit de83646

Browse files
fmt: apply cargo fmt changes
1 parent 7e36aa5 commit de83646

File tree

2 files changed

+163
-44
lines changed

2 files changed

+163
-44
lines changed

crates/djls-project/src/finder.rs

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use std::path::PathBuf;
2+
use which::Error as WhichError;
3+
4+
// --- Public API ---
5+
6+
pub fn find(name: &str) -> Result<PathBuf, WhichError> {
7+
#[cfg(not(test))]
8+
{
9+
which::which(name)
10+
}
11+
#[cfg(test)]
12+
{
13+
mock::find_mocked(name)
14+
}
15+
}
16+
17+
// --- Test-only Mocking Logic ---
18+
19+
#[cfg(test)]
20+
pub mod mock {
21+
use super::*;
22+
use std::cell::RefCell;
23+
use std::collections::HashMap;
24+
use std::thread_local;
25+
26+
// Store mock results per thread to avoid test interference
27+
thread_local! {
28+
static MOCK_RESULTS: RefCell<HashMap<String, Result<PathBuf, WhichError>>> = RefCell::new(HashMap::new());
29+
}
30+
31+
// Internal function called by the public `find` when cfg(test) is enabled.
32+
// Returns the mocked result for `name` or a default error if not mocked.
33+
pub(super) fn find_mocked(name: &str) -> Result<PathBuf, WhichError> {
34+
MOCK_RESULTS.with(|mocks| {
35+
let mocks = mocks.borrow();
36+
mocks.get(name).cloned().unwrap_or_else(|| {
37+
Err(WhichError::CannotFindBinaryPath)
38+
})
39+
})
40+
}
41+
42+
// --- Test Setup ---
43+
44+
// RAII guard to ensure mock state is cleared after each test.
45+
pub struct MockGuard;
46+
47+
impl Drop for MockGuard {
48+
fn drop(&mut self) {
49+
MOCK_RESULTS.with(|mocks| mocks.borrow_mut().clear());
50+
}
51+
}
52+
53+
// Sets a mock successful result for `find(name)`.
54+
pub fn set_mock_path(name: &str, path: PathBuf) {
55+
MOCK_RESULTS.with(|mocks| {
56+
mocks.borrow_mut().insert(name.to_string(), Ok(path));
57+
});
58+
}
59+
60+
// Sets a mock error result for `find(name)`.
61+
pub fn set_mock_error(name: &str, error: WhichError) {
62+
MOCK_RESULTS.with(|mocks| {
63+
mocks.borrow_mut().insert(name.to_string(), Err(error));
64+
});
65+
}
66+
} // end mod mock
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use super::mock::{self as finder_mock, MockGuard};
71+
use super::*; // Import find function etc.
72+
use std::path::PathBuf;
73+
use which::Error as WhichError;
74+
75+
#[test]
76+
fn test_mock_path_retrieval() {
77+
let _guard = MockGuard;
78+
let expected_path = PathBuf::from("/mock/path/to/python");
79+
finder_mock::set_mock_path("python", expected_path.clone());
80+
81+
let result = find("python");
82+
assert_eq!(result.unwrap(), expected_path);
83+
}
84+
85+
#[test]
86+
fn test_mock_error_retrieval() {
87+
let _guard = MockGuard;
88+
finder_mock::set_mock_error("cargo", WhichError::CannotFindBinaryPath);
89+
90+
let result = find("cargo");
91+
assert!(matches!(result, Err(WhichError::CannotFindBinaryPath)));
92+
}
93+
94+
#[test]
95+
fn test_mock_default_error_if_unmocked() {
96+
let _guard = MockGuard;
97+
let result = find("git"); // "git" is not mocked
98+
assert!(matches!(result, Err(WhichError::CannotFindBinaryPath)));
99+
}
100+
101+
#[test]
102+
fn test_mock_guard_clears_mocks() {
103+
let expected_path = PathBuf::from("/tmp/myprog");
104+
105+
{
106+
let _guard = MockGuard;
107+
finder_mock::set_mock_path("myprog", expected_path.clone());
108+
// Mock cleared when _guard drops here
109+
}
110+
111+
// Verify mock was cleared
112+
let _guard = MockGuard; // Need guard for this scope too
113+
let result = find("myprog");
114+
// Should default back to error because the previous mock was cleared
115+
assert!(matches!(result, Err(WhichError::CannotFindBinaryPath)));
116+
}
117+
118+
#[test]
119+
fn test_mocks_are_separate_between_tests() {
120+
// This test relies on cargo running tests sequentially or in separate threads
121+
// where the thread_local works correctly. If tests were run concurrently
122+
// in the *same* thread, this could fail, but that's not the default cargo behavior.
123+
124+
let _guard = MockGuard; // Clears any state from previous tests (like test_mock_path_retrieval)
125+
126+
// Ensure "python" is not mocked here, even if set in another test
127+
let result_python = find("python");
128+
assert!(matches!(result_python, Err(WhichError::CannotFindBinaryPath)));
129+
130+
// Set a mock specific to this test
131+
let expected_path_node = PathBuf::from("/usr/bin/node");
132+
finder_mock::set_mock_path("node", expected_path_node.clone());
133+
let result_node = find("node");
134+
assert_eq!(result_node.unwrap(), expected_path_node);
135+
}
136+
}
137+

crates/djls-project/src/lib.rs

Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ use pyo3::prelude::*;
66
use std::env;
77
use std::fmt;
88
use std::path::{Path, PathBuf};
9-
use which::which;
9+
10+
mod finder;
1011

1112
#[derive(Debug)]
1213
pub struct DjangoProject {
@@ -126,7 +127,6 @@ impl PythonEnvironment {
126127
#[cfg(windows)]
127128
let python_path = prefix.join("Scripts").join("python.exe");
128129

129-
// Check if the *prefix* and the *binary* exist.
130130
if !prefix.is_dir() || !python_path.exists() {
131131
return None;
132132
}
@@ -137,10 +137,9 @@ impl PythonEnvironment {
137137
let bin_dir = prefix.join("Scripts");
138138

139139
let mut sys_path = Vec::new();
140-
sys_path.push(bin_dir); // Add bin/ or Scripts/
140+
sys_path.push(bin_dir);
141141

142142
if let Some(site_packages) = Self::find_site_packages(prefix) {
143-
// Check existence inside the if let, as find_site_packages might return a path that doesn't exist
144143
if site_packages.is_dir() {
145144
sys_path.push(site_packages);
146145
}
@@ -167,12 +166,10 @@ impl PythonEnvironment {
167166
}
168167

169168
fn from_system_python() -> Option<Self> {
170-
let python_path = match which("python") {
169+
let python_path = match finder::find("python") {
171170
Ok(p) => p,
172171
Err(_) => return None,
173172
};
174-
// which() might return a path inside a bin/Scripts dir, or directly the executable
175-
// We need the prefix, which is usually two levels up from the executable in standard layouts
176173
let bin_dir = python_path.parent()?;
177174
let prefix = bin_dir.parent()?;
178175

@@ -237,6 +234,8 @@ mod tests {
237234

238235
mod env_discovery {
239236
use super::*;
237+
use super::finder::mock::{self as exec_mock, MockGuard};
238+
use which::Error as WhichError;
240239

241240
fn create_mock_venv(dir: &Path, version: Option<&str>) -> PathBuf {
242241
let prefix = dir.to_path_buf();
@@ -291,29 +290,6 @@ mod tests {
291290
}
292291
}
293292

294-
// Guard struct to temporarily modify the PATH environment variable
295-
struct PathGuard {
296-
original_path: Option<String>,
297-
}
298-
299-
impl PathGuard {
300-
fn set(new_path_val: &str) -> Self {
301-
let original_path = env::var("PATH").ok();
302-
env::set_var("PATH", new_path_val);
303-
Self { original_path }
304-
}
305-
}
306-
307-
impl Drop for PathGuard {
308-
fn drop(&mut self) {
309-
// Restore original PATH, or remove if it wasn't set initially
310-
match self.original_path.as_deref() {
311-
Some(val) => env::set_var("PATH", val),
312-
None => env::remove_var("PATH"),
313-
}
314-
}
315-
}
316-
317293
#[test]
318294
fn test_explicit_venv_path_found() {
319295
let project_dir = tempdir().unwrap();
@@ -485,16 +461,13 @@ mod tests {
485461
let site_packages_path = mock_sys_python_prefix.join(site_packages_rel_path);
486462
fs::create_dir_all(&site_packages_path).unwrap();
487463

488-
// --- Manipulate PATH ---
489-
// Completely overwrite PATH to only include the mock bin directory
490-
let canonical_bin_dir =
491-
bin_dir.canonicalize().expect("Failed to canonicalize mock bin dir");
492-
let new_path = canonical_bin_dir.to_str().unwrap().to_string();
493-
let _path_guard = PathGuard::set(&new_path);
464+
// --- Set up executable finder mock ---
465+
let _guard = MockGuard; // Ensure mocks are cleared after test
466+
exec_mock::set_mock_path("python", python_path.clone());
494467

495468
// We don't create any venvs in project_dir
496469

497-
// This test assumes `which python` works and points to a standard layout
470+
// This test now assumes the mock is correctly configured
498471
let system_env = PythonEnvironment::new(project_dir.path(), None);
499472

500473
assert!(
@@ -503,10 +476,12 @@ mod tests {
503476
);
504477

505478
if let Some(env) = system_env {
506-
assert_eq!(env.python_path, python_path, "Python path should match mock");
507479
assert_eq!(
508-
env.sys_prefix,
509-
mock_sys_python_prefix,
480+
env.python_path, python_path,
481+
"Python path should match mock"
482+
);
483+
assert_eq!(
484+
env.sys_prefix, mock_sys_python_prefix,
510485
"Sys prefix should match mock prefix"
511486
);
512487
assert!(
@@ -528,17 +503,24 @@ mod tests {
528503

529504
// Ensure no explicit path, no project venvs, and set VIRTUAL_ENV to invalid.
530505
let invalid_virtual_env_path = project_dir.path().join("non_existent_venv_no_python");
531-
let _guard =
506+
let _guard_venv =
532507
VirtualEnvGuard::set("VIRTUAL_ENV", invalid_virtual_env_path.to_str().unwrap());
533508

534-
// Overwrite PATH with an empty value to ensure `which("python")` fails.
535-
let _path_guard = PathGuard::set("");
509+
// --- Set up executable finder mock to fail ---
510+
let _guard_mock = MockGuard;
511+
exec_mock::set_mock_error(
512+
"python",
513+
WhichError::CannotFindBinaryPath,
514+
);
536515

537516
// Call the function under test
538517
let env = PythonEnvironment::new(project_dir.path(), None);
539518

540519
// Assert that no environment was found
541-
assert!(env.is_none(), "Expected no environment to be found when all discovery methods fail");
520+
assert!(
521+
env.is_none(),
522+
"Expected no environment to be found when all discovery methods fail"
523+
);
542524
}
543525

544526
#[test]

0 commit comments

Comments
 (0)