@@ -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,180 @@ 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+ let expected_executable = env. executable . clone ( ) ;
309+ let expected_prefix = env. prefix . clone ( ) ;
310+
311+ let environment = get_python_in_bin ( & env, true ) . unwrap ( ) ;
312+
313+ assert_eq ! ( environment. kind, Some ( PythonEnvironmentKind :: LinuxGlobal ) ) ;
314+ assert_eq ! ( environment. executable, Some ( expected_executable. clone( ) ) ) ;
315+ assert_eq ! ( environment. prefix, expected_prefix) ;
316+ assert_eq ! ( environment. version, Some ( "3.12.1" . to_string( ) ) ) ;
317+ assert_eq ! ( environment. arch, Some ( Architecture :: X64 ) ) ;
318+ assert ! ( environment. symlinks. unwrap( ) . contains( & expected_executable) ) ;
319+ }
320+
321+ #[ test]
322+ fn get_python_in_bin_reports_x86_when_not_64_bit ( ) {
323+ let dir = tempdir ( ) . unwrap ( ) ;
324+ let executable = dir. path ( ) . join ( PYTHON_EXE ) ;
325+ create_executable ( & executable) ;
326+ let env = create_env ( executable, dir. path ( ) . to_path_buf ( ) ) ;
327+
328+ let environment = get_python_in_bin ( & env, false ) . unwrap ( ) ;
329+
330+ assert_eq ! ( environment. arch, Some ( Architecture :: X86 ) ) ;
331+ }
332+
333+ #[ test]
334+ fn get_python_in_bin_preserves_and_dedupes_known_symlinks ( ) {
335+ let dir = tempdir ( ) . unwrap ( ) ;
336+ let executable = dir. path ( ) . join ( PYTHON_EXE ) ;
337+ let known_symlink = dir. path ( ) . join ( PYTHON_VERSIONED_EXE ) ;
338+ create_executable ( & executable) ;
339+ create_executable ( & known_symlink) ;
340+ let mut env = create_env ( executable. clone ( ) , dir. path ( ) . to_path_buf ( ) ) ;
341+ env. symlinks = Some ( vec ! [ known_symlink. clone( ) , executable. clone( ) ] ) ;
342+
343+ let environment = get_python_in_bin ( & env, true ) . unwrap ( ) ;
344+ let symlinks = environment. symlinks . unwrap ( ) ;
345+
346+ assert_eq ! (
347+ symlinks. iter( ) . filter( |path| * path == & executable) . count( ) ,
348+ 1
349+ ) ;
350+ assert ! ( symlinks. contains( & known_symlink) ) ;
351+ }
352+
353+ #[ cfg( unix) ]
354+ #[ test]
355+ fn get_python_in_bin_collects_same_directory_symlink_target ( ) {
356+ use std:: os:: unix:: fs:: symlink;
357+
358+ let dir = tempdir ( ) . unwrap ( ) ;
359+ let executable = dir. path ( ) . join ( "python3" ) ;
360+ let versioned_executable = dir. path ( ) . join ( PYTHON_VERSIONED_EXE ) ;
361+ create_executable ( & versioned_executable) ;
362+ symlink ( & versioned_executable, & executable) . unwrap ( ) ;
363+ let env = create_env ( executable. clone ( ) , dir. path ( ) . to_path_buf ( ) ) ;
364+
365+ let environment = get_python_in_bin ( & env, true ) . unwrap ( ) ;
366+ let symlinks = environment. symlinks . unwrap ( ) ;
367+
368+ assert ! ( symlinks. contains( & executable) ) ;
369+ assert ! ( symlinks. contains( & versioned_executable) ) ;
370+ }
371+
372+ #[ cfg( unix) ]
373+ #[ test]
374+ fn get_python_in_bin_keeps_cross_directory_symlink_separate ( ) {
375+ use std:: os:: unix:: fs:: symlink;
376+
377+ let link_dir = tempdir ( ) . unwrap ( ) ;
378+ let real_dir = tempdir ( ) . unwrap ( ) ;
379+ let executable = link_dir. path ( ) . join ( "python3" ) ;
380+ let real_executable = real_dir. path ( ) . join ( PYTHON_VERSIONED_EXE ) ;
381+ create_executable ( & real_executable) ;
382+ symlink ( & real_executable, & executable) . unwrap ( ) ;
383+ let env = create_env ( executable. clone ( ) , link_dir. path ( ) . to_path_buf ( ) ) ;
384+
385+ let environment = get_python_in_bin ( & env, true ) . unwrap ( ) ;
386+ let symlinks = environment. symlinks . unwrap ( ) ;
387+
388+ assert ! ( symlinks. contains( & executable) ) ;
389+ assert ! ( !symlinks. contains( & real_executable) ) ;
390+ }
391+
392+ #[ test]
393+ fn try_from_returns_none_without_version_before_cache_lookup ( ) {
394+ let locator = LinuxGlobalPython :: new ( ) ;
395+ let env = PythonEnv :: new (
396+ PathBuf :: from ( "/usr/bin/python3" ) ,
397+ Some ( PathBuf :: from ( "/usr" ) ) ,
398+ None ,
399+ ) ;
400+
401+ assert ! ( locator. try_from( & env) . is_none( ) ) ;
402+ assert ! ( locator. reported_executables. is_empty( ) ) ;
403+ }
404+
405+ #[ cfg( not( any( target_os = "macos" , target_os = "windows" ) ) ) ]
406+ #[ test]
407+ fn try_from_rejects_non_global_path_before_cache_lookup ( ) {
408+ let dir = tempdir ( ) . unwrap ( ) ;
409+ let executable = dir. path ( ) . join ( "python" ) ;
410+ create_executable ( & executable) ;
411+ let locator = LinuxGlobalPython :: new ( ) ;
412+ let env = PythonEnv :: new (
413+ executable,
414+ Some ( dir. path ( ) . to_path_buf ( ) ) ,
415+ Some ( "3.12.1" . to_string ( ) ) ,
416+ ) ;
417+
418+ assert ! ( locator. try_from( & env) . is_none( ) ) ;
419+ assert ! ( locator. reported_executables. is_empty( ) ) ;
420+ }
421+
422+ #[ cfg( not( any( target_os = "macos" , target_os = "windows" ) ) ) ]
423+ #[ test]
424+ fn try_from_rejects_virtualenv_before_cache_lookup ( ) {
425+ let dir = tempdir ( ) . unwrap ( ) ;
426+ let bin_dir = dir. path ( ) . join ( "bin" ) ;
427+ fs:: create_dir_all ( & bin_dir) . unwrap ( ) ;
428+ fs:: write ( bin_dir. join ( "activate" ) , b"" ) . unwrap ( ) ;
429+ let executable = bin_dir. join ( "python" ) ;
430+ create_executable ( & executable) ;
431+ let locator = LinuxGlobalPython :: new ( ) ;
432+ let env = PythonEnv :: new (
433+ executable,
434+ Some ( dir. path ( ) . to_path_buf ( ) ) ,
435+ Some ( "3.12.1" . to_string ( ) ) ,
436+ ) ;
437+
438+ assert ! ( locator. try_from( & env) . is_none( ) ) ;
439+ assert ! ( locator. reported_executables. is_empty( ) ) ;
440+ }
441+ }
0 commit comments