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