@@ -85,8 +85,6 @@ impl Locator for LinuxGlobalPython {
8585 env. version . clone ( ) ?;
8686 let executable = env. executable . clone ( ) ;
8787
88- self . find_cached ( None ) ;
89-
9088 // Resolve the canonical path once — used for both the path guard and cache fallback.
9189 let canonical = fs:: canonicalize ( & executable) . ok ( ) ;
9290
@@ -100,6 +98,8 @@ impl Locator for LinuxGlobalPython {
10098 return None ;
10199 }
102100
101+ self . find_cached ( None ) ;
102+
103103 // Try direct cache lookup first.
104104 if let Some ( env) = self . reported_executables . get ( & executable) {
105105 return Some ( env) ;
@@ -262,3 +262,178 @@ fn get_python_in_bin(env: &PythonEnv, is_64bit: bool) -> Option<PythonEnvironmen
262262 . build ( ) ,
263263 )
264264}
265+
266+ #[ cfg( test) ]
267+ mod tests {
268+ use super :: * ;
269+ use pet_core:: python_environment:: PythonEnvironmentKind ;
270+ use std:: fs;
271+ use tempfile:: tempdir;
272+
273+ #[ cfg( windows) ]
274+ const PYTHON_EXE : & str = "python.exe" ;
275+ #[ cfg( not( windows) ) ]
276+ const PYTHON_EXE : & str = "python" ;
277+
278+ #[ cfg( windows) ]
279+ const PYTHON_VERSIONED_EXE : & str = "python3.12.exe" ;
280+ #[ cfg( not( windows) ) ]
281+ const PYTHON_VERSIONED_EXE : & str = "python3.12" ;
282+
283+ fn create_executable ( path : & Path ) {
284+ fs:: write ( path, b"" ) . unwrap ( ) ;
285+ }
286+
287+ fn create_env ( executable : PathBuf , prefix : PathBuf ) -> PythonEnv {
288+ PythonEnv :: new ( executable, Some ( prefix) , Some ( "3.12.1" . to_string ( ) ) )
289+ }
290+
291+ #[ test]
292+ fn get_python_in_bin_requires_version_and_prefix ( ) {
293+ let dir = tempdir ( ) . unwrap ( ) ;
294+ let executable = dir. path ( ) . join ( PYTHON_EXE ) ;
295+ let versionless = PythonEnv :: new ( executable. clone ( ) , Some ( dir. path ( ) . to_path_buf ( ) ) , None ) ;
296+ let prefixless = PythonEnv :: new ( executable, None , Some ( "3.12.1" . to_string ( ) ) ) ;
297+
298+ assert ! ( get_python_in_bin( & versionless, true ) . is_none( ) ) ;
299+ assert ! ( get_python_in_bin( & prefixless, true ) . is_none( ) ) ;
300+ }
301+
302+ #[ test]
303+ fn get_python_in_bin_builds_linux_global_environment ( ) {
304+ let dir = tempdir ( ) . unwrap ( ) ;
305+ let executable = dir. path ( ) . join ( PYTHON_EXE ) ;
306+ create_executable ( & executable) ;
307+ let env = create_env ( executable. clone ( ) , dir. path ( ) . to_path_buf ( ) ) ;
308+
309+ let environment = get_python_in_bin ( & env, true ) . unwrap ( ) ;
310+
311+ assert_eq ! ( environment. kind, Some ( PythonEnvironmentKind :: LinuxGlobal ) ) ;
312+ assert_eq ! ( environment. executable, Some ( executable. clone( ) ) ) ;
313+ assert_eq ! ( environment. prefix, Some ( dir. path( ) . to_path_buf( ) ) ) ;
314+ assert_eq ! ( environment. version, Some ( "3.12.1" . to_string( ) ) ) ;
315+ assert_eq ! ( environment. arch, Some ( Architecture :: X64 ) ) ;
316+ assert ! ( environment. symlinks. unwrap( ) . contains( & executable) ) ;
317+ }
318+
319+ #[ test]
320+ fn get_python_in_bin_reports_x86_when_not_64_bit ( ) {
321+ let dir = tempdir ( ) . unwrap ( ) ;
322+ let executable = dir. path ( ) . join ( PYTHON_EXE ) ;
323+ create_executable ( & executable) ;
324+ let env = create_env ( executable, dir. path ( ) . to_path_buf ( ) ) ;
325+
326+ let environment = get_python_in_bin ( & env, false ) . unwrap ( ) ;
327+
328+ assert_eq ! ( environment. arch, Some ( Architecture :: X86 ) ) ;
329+ }
330+
331+ #[ test]
332+ fn get_python_in_bin_preserves_and_dedupes_known_symlinks ( ) {
333+ let dir = tempdir ( ) . unwrap ( ) ;
334+ let executable = dir. path ( ) . join ( PYTHON_EXE ) ;
335+ let known_symlink = dir. path ( ) . join ( PYTHON_VERSIONED_EXE ) ;
336+ create_executable ( & executable) ;
337+ create_executable ( & known_symlink) ;
338+ let mut env = create_env ( executable. clone ( ) , dir. path ( ) . to_path_buf ( ) ) ;
339+ env. symlinks = Some ( vec ! [ known_symlink. clone( ) , executable. clone( ) ] ) ;
340+
341+ let environment = get_python_in_bin ( & env, true ) . unwrap ( ) ;
342+ let symlinks = environment. symlinks . unwrap ( ) ;
343+
344+ assert_eq ! (
345+ symlinks. iter( ) . filter( |path| * path == & executable) . count( ) ,
346+ 1
347+ ) ;
348+ assert ! ( symlinks. contains( & known_symlink) ) ;
349+ }
350+
351+ #[ cfg( unix) ]
352+ #[ test]
353+ fn get_python_in_bin_collects_same_directory_symlink_target ( ) {
354+ use std:: os:: unix:: fs:: symlink;
355+
356+ let dir = tempdir ( ) . unwrap ( ) ;
357+ let executable = dir. path ( ) . join ( "python3" ) ;
358+ let versioned_executable = dir. path ( ) . join ( PYTHON_VERSIONED_EXE ) ;
359+ create_executable ( & versioned_executable) ;
360+ symlink ( & versioned_executable, & executable) . unwrap ( ) ;
361+ let env = create_env ( executable. clone ( ) , dir. path ( ) . to_path_buf ( ) ) ;
362+
363+ let environment = get_python_in_bin ( & env, true ) . unwrap ( ) ;
364+ let symlinks = environment. symlinks . unwrap ( ) ;
365+
366+ assert ! ( symlinks. contains( & executable) ) ;
367+ assert ! ( symlinks. contains( & versioned_executable) ) ;
368+ }
369+
370+ #[ cfg( unix) ]
371+ #[ test]
372+ fn get_python_in_bin_keeps_cross_directory_symlink_separate ( ) {
373+ use std:: os:: unix:: fs:: symlink;
374+
375+ let link_dir = tempdir ( ) . unwrap ( ) ;
376+ let real_dir = tempdir ( ) . unwrap ( ) ;
377+ let executable = link_dir. path ( ) . join ( "python3" ) ;
378+ let real_executable = real_dir. path ( ) . join ( PYTHON_VERSIONED_EXE ) ;
379+ create_executable ( & real_executable) ;
380+ symlink ( & real_executable, & executable) . unwrap ( ) ;
381+ let env = create_env ( executable. clone ( ) , link_dir. path ( ) . to_path_buf ( ) ) ;
382+
383+ let environment = get_python_in_bin ( & env, true ) . unwrap ( ) ;
384+ let symlinks = environment. symlinks . unwrap ( ) ;
385+
386+ assert ! ( symlinks. contains( & executable) ) ;
387+ assert ! ( !symlinks. contains( & real_executable) ) ;
388+ }
389+
390+ #[ test]
391+ fn try_from_returns_none_without_version_before_cache_lookup ( ) {
392+ let locator = LinuxGlobalPython :: new ( ) ;
393+ let env = PythonEnv :: new (
394+ PathBuf :: from ( "/usr/bin/python3" ) ,
395+ Some ( PathBuf :: from ( "/usr" ) ) ,
396+ None ,
397+ ) ;
398+
399+ assert ! ( locator. try_from( & env) . is_none( ) ) ;
400+ assert ! ( locator. reported_executables. is_empty( ) ) ;
401+ }
402+
403+ #[ cfg( not( any( target_os = "macos" , target_os = "windows" ) ) ) ]
404+ #[ test]
405+ fn try_from_rejects_non_global_path_before_cache_lookup ( ) {
406+ let dir = tempdir ( ) . unwrap ( ) ;
407+ let executable = dir. path ( ) . join ( "python" ) ;
408+ create_executable ( & executable) ;
409+ let locator = LinuxGlobalPython :: new ( ) ;
410+ let env = PythonEnv :: new (
411+ executable,
412+ Some ( dir. path ( ) . to_path_buf ( ) ) ,
413+ Some ( "3.12.1" . to_string ( ) ) ,
414+ ) ;
415+
416+ assert ! ( locator. try_from( & env) . is_none( ) ) ;
417+ assert ! ( locator. reported_executables. is_empty( ) ) ;
418+ }
419+
420+ #[ cfg( not( any( target_os = "macos" , target_os = "windows" ) ) ) ]
421+ #[ test]
422+ fn try_from_rejects_virtualenv_before_cache_lookup ( ) {
423+ let dir = tempdir ( ) . unwrap ( ) ;
424+ let bin_dir = dir. path ( ) . join ( "bin" ) ;
425+ fs:: create_dir_all ( & bin_dir) . unwrap ( ) ;
426+ fs:: write ( bin_dir. join ( "activate" ) , b"" ) . unwrap ( ) ;
427+ let executable = bin_dir. join ( "python" ) ;
428+ create_executable ( & executable) ;
429+ let locator = LinuxGlobalPython :: new ( ) ;
430+ let env = PythonEnv :: new (
431+ executable,
432+ Some ( dir. path ( ) . to_path_buf ( ) ) ,
433+ Some ( "3.12.1" . to_string ( ) ) ,
434+ ) ;
435+
436+ assert ! ( locator. try_from( & env) . is_none( ) ) ;
437+ assert ! ( locator. reported_executables. is_empty( ) ) ;
438+ }
439+ }
0 commit comments