1use crate::errors::BridgeError;
8use r2x_config::venv_paths::{resolve_python_exe, resolve_site_packages, VenvPathError};
9use std::path::{Path, PathBuf};
10
11#[cfg(test)]
12const PYTHON_LIB_DIR: &str = r2x_config::venv_paths::PYTHON_LIB_DIR;
13#[cfg(test)]
14const PYTHON_BIN_DIR: &str = r2x_config::venv_paths::PYTHON_BIN_DIR;
15
16pub fn resolve_site_package_path(venv_path: &Path) -> Result<PathBuf, BridgeError> {
17 resolve_site_packages(venv_path).map_err(venv_path_error_to_bridge_error)
18}
19
20pub(crate) fn resolve_python_path(venv_path: &Path) -> Result<PathBuf, BridgeError> {
21 resolve_python_exe(venv_path).map_err(venv_path_error_to_bridge_error)
22}
23
24fn venv_path_error_to_bridge_error(error: VenvPathError) -> BridgeError {
25 match error {
26 VenvPathError::VenvNotFound(path) => BridgeError::VenvNotFound(path),
27 VenvPathError::PathResolution(message) => BridgeError::Initialization(message),
28 }
29}
30
31#[cfg(test)]
32mod tests {
33 use crate::errors::BridgeError;
34 use crate::utils::*;
35 use std::fs;
36 use std::path::PathBuf;
37 use tempfile::TempDir;
38
39 #[cfg(not(windows))]
40 fn create_mock_venv_unix(python_version: &str) -> Option<TempDir> {
41 let temp_dir = TempDir::new().ok()?;
42 let venv_path = temp_dir.path();
43
44 let site_packages = venv_path
45 .join(PYTHON_LIB_DIR)
46 .join(python_version)
47 .join("site-packages");
48 fs::create_dir_all(&site_packages).ok()?;
49
50 let bin_dir = venv_path.join(PYTHON_BIN_DIR);
51 fs::create_dir_all(&bin_dir).ok()?;
52 fs::write(bin_dir.join("python3"), "").ok()?;
53
54 Some(temp_dir)
55 }
56
57 #[cfg(windows)]
58 fn create_mock_venv_windows(python_exe: &str) -> Option<TempDir> {
59 let temp_dir = TempDir::new().ok()?;
60 let venv_path = temp_dir.path();
61
62 let site_packages = venv_path.join(PYTHON_LIB_DIR).join("site-packages");
63 fs::create_dir_all(&site_packages).ok()?;
64
65 let scripts_dir = venv_path.join(PYTHON_BIN_DIR);
66 fs::create_dir_all(&scripts_dir).ok()?;
67 fs::write(scripts_dir.join(python_exe), "").ok()?;
68
69 Some(temp_dir)
70 }
71
72 #[test]
73 #[cfg(not(windows))]
74 fn test_resolve_site_package_path_unix() {
75 let Some(temp_venv) = create_mock_venv_unix("python3.12") else {
76 return;
77 };
78
79 let result = resolve_site_package_path(temp_venv.path());
80 assert!(result.is_ok_and(|sp| sp.ends_with("lib/python3.12/site-packages")));
81 }
82
83 #[test]
84 #[cfg(not(windows))]
85 fn test_resolve_site_package_path_unix_different_version() {
86 let Some(temp_venv) = create_mock_venv_unix("python3.11") else {
87 return;
88 };
89
90 let result = resolve_site_package_path(temp_venv.path());
91 assert!(result.is_ok_and(|sp| sp.ends_with("lib/python3.11/site-packages")));
92 }
93
94 #[test]
95 #[cfg(windows)]
96 fn test_resolve_site_package_path_windows() {
97 let Some(temp_venv) = create_mock_venv_windows("python.exe") else {
98 return;
99 };
100
101 let result = resolve_site_package_path(temp_venv.path());
102 assert!(result.is_ok_and(|sp| sp.ends_with("Lib\\site-packages")));
103 }
104
105 #[test]
106 fn test_resolve_site_package_path_venv_not_found() {
107 let non_existent_path = PathBuf::from("/tmp/non_existent_venv_12345");
108
109 let result = resolve_site_package_path(&non_existent_path);
110 assert!(matches!(
111 result,
112 Err(BridgeError::VenvNotFound(path)) if path == non_existent_path
113 ));
114 }
115
116 #[test]
117 #[cfg(not(windows))]
118 fn test_resolve_site_package_path_missing_python_dir() {
119 let Ok(temp_dir) = TempDir::new() else {
120 return;
121 };
122 let lib_dir = temp_dir.path().join(PYTHON_LIB_DIR);
123 if fs::create_dir_all(&lib_dir).is_err() {
124 return;
125 }
126
127 let result = resolve_site_package_path(temp_dir.path());
128 assert!(result.is_err_and(|e| {
129 matches!(e, BridgeError::Initialization(msg) if msg.contains("No python3.X directory found"))
130 }));
131 }
132
133 #[test]
134 #[cfg(not(windows))]
135 fn test_resolve_python_path_unix() {
136 let Some(temp_venv) = create_mock_venv_unix("python3.12") else {
137 return;
138 };
139
140 let result = resolve_python_path(temp_venv.path());
141 assert!(result.is_ok_and(|pp| pp.ends_with("bin/python3")));
142 }
143
144 #[test]
145 #[cfg(not(windows))]
146 fn test_resolve_python_path_unix_versioned_fallback() {
147 let Some(temp_venv) = create_mock_venv_unix("python3.13") else {
148 return;
149 };
150 let bin_dir = temp_venv.path().join(PYTHON_BIN_DIR);
151 if fs::remove_file(bin_dir.join("python3")).is_err() {
152 return;
153 }
154 if fs::write(bin_dir.join("python3.13"), "").is_err() {
155 return;
156 }
157
158 let result = resolve_python_path(temp_venv.path());
159 assert!(result.is_ok_and(|pp| pp.ends_with("bin/python3.13")));
160 }
161
162 #[test]
163 #[cfg(windows)]
164 fn test_resolve_python_path_windows() {
165 let Some(temp_venv) = create_mock_venv_windows("python.exe") else {
166 return;
167 };
168
169 let result = resolve_python_path(temp_venv.path());
170 assert!(result.is_ok_and(|pp| pp.ends_with("Scripts\\python.exe")));
171 }
172
173 #[test]
174 #[cfg(windows)]
175 fn test_resolve_python_path_windows_versioned_fallback() {
176 let Some(temp_venv) = create_mock_venv_windows("python3.13.exe") else {
177 return;
178 };
179
180 let result = resolve_python_path(temp_venv.path());
181 assert!(result.is_ok_and(|pp| pp.ends_with("Scripts\\python3.13.exe")));
182 }
183
184 #[test]
185 fn test_python_lib_dir_constant() {
186 #[cfg(not(windows))]
187 assert_eq!(PYTHON_LIB_DIR, "lib");
188
189 #[cfg(windows)]
190 assert_eq!(PYTHON_LIB_DIR, "Lib");
191 }
192
193 #[test]
194 fn test_python_bin_dir_constant() {
195 #[cfg(not(windows))]
196 assert_eq!(PYTHON_BIN_DIR, "bin");
197
198 #[cfg(windows)]
199 assert_eq!(PYTHON_BIN_DIR, "Scripts");
200 }
201
202 #[test]
203 #[cfg(not(windows))]
204 fn test_resolve_site_package_path_with_multiple_python_versions() {
205 let Ok(temp_dir) = TempDir::new() else {
206 return;
207 };
208 let lib_dir = temp_dir.path().join(PYTHON_LIB_DIR);
209 if fs::create_dir_all(&lib_dir).is_err() {
210 return;
211 }
212
213 for python_version in ["python3.11", "python3.12"] {
214 let site_packages = lib_dir.join(python_version).join("site-packages");
215 if fs::create_dir_all(&site_packages).is_err() {
216 return;
217 }
218 }
219
220 let result = resolve_site_package_path(temp_dir.path());
221 assert!(result.is_ok_and(
222 |sp| sp.to_string_lossy().contains("python3.1") && sp.ends_with("site-packages")
223 ));
224 }
225}