@@ -21,7 +21,7 @@ pub fn report_inaccuracies_identified_after_resolving(
2121 _reporter : & dyn Reporter ,
2222 env : & PythonEnvironment ,
2323 resolved : & PythonEnvironment ,
24- ) -> Option < ( ) > {
24+ ) -> Option < InaccuratePythonEnvironmentInfo > {
2525 let known_symlinks = env. symlinks . clone ( ) . unwrap_or_default ( ) ;
2626 let resolved_executable = & resolved. executable . clone ( ) ?;
2727 let norm_cased_executable = norm_case ( resolved_executable) ;
@@ -39,10 +39,21 @@ pub fn report_inaccuracies_identified_after_resolving(
3939 executable_not_in_symlinks = false ;
4040 }
4141
42- let mut invalid_prefix = env. prefix . clone ( ) . unwrap_or_default ( ) != resolved. prefix . clone ( ) ?;
43- if env. prefix . clone ( ) . is_none ( ) {
44- invalid_prefix = false ;
45- }
42+ let invalid_prefix = if let Some ( ref env_prefix) = env. prefix {
43+ let resolved_prefix = resolved. prefix . clone ( ) ?;
44+ // Canonicalize both paths to handle symlinks — a venv prefix like
45+ // /usr/local/venvs/myvenv may be a symlink to /usr/local/venvs/versioned/myvenv-1.0.51,
46+ // and sys.prefix returns the resolved target. Without this, the comparison
47+ // produces a false positive "Prefix is incorrect" warning. (See #358)
48+ // Wrap in norm_case to handle Windows UNC prefix (`\\?\`) from canonicalize.
49+ let env_canonical =
50+ norm_case ( std:: fs:: canonicalize ( env_prefix) . unwrap_or ( env_prefix. clone ( ) ) ) ;
51+ let resolved_canonical =
52+ norm_case ( std:: fs:: canonicalize ( & resolved_prefix) . unwrap_or ( resolved_prefix) ) ;
53+ env_canonical != resolved_canonical
54+ } else {
55+ false
56+ } ;
4657
4758 let mut invalid_arch = env. arch . clone ( ) != resolved. arch . clone ( ) ;
4859 if env. arch . clone ( ) . is_none ( ) {
@@ -73,8 +84,9 @@ pub fn report_inaccuracies_identified_after_resolving(
7384 env, resolved, event
7485 ) ;
7586 // reporter.report_telemetry(TelemetryEvent::InaccuratePythonEnvironmentInfo(event));
87+ return Some ( event) ;
7688 }
77- Option :: Some ( ( ) )
89+ None
7890}
7991
8092fn are_versions_different ( actual : & str , expected : & str ) -> Option < bool > {
@@ -84,3 +96,110 @@ fn are_versions_different(actual: &str, expected: &str) -> Option<bool> {
8496 let expected = expected. get ( 1 ) ?. as_str ( ) . to_string ( ) ;
8597 Some ( actual != expected)
8698}
99+
100+ #[ cfg( test) ]
101+ mod tests {
102+ use super :: * ;
103+ use pet_core:: {
104+ manager:: EnvManager ,
105+ python_environment:: { PythonEnvironmentBuilder , PythonEnvironmentKind } ,
106+ telemetry:: TelemetryEvent ,
107+ } ;
108+ use std:: path:: PathBuf ;
109+
110+ struct NoopReporter ;
111+ impl Reporter for NoopReporter {
112+ fn report_manager ( & self , _: & EnvManager ) { }
113+ fn report_environment ( & self , _: & PythonEnvironment ) { }
114+ fn report_telemetry ( & self , _: & TelemetryEvent ) { }
115+ }
116+
117+ fn make_env (
118+ executable : PathBuf ,
119+ prefix : PathBuf ,
120+ version : & str ,
121+ symlinks : Vec < PathBuf > ,
122+ ) -> PythonEnvironment {
123+ PythonEnvironmentBuilder :: new ( Some ( PythonEnvironmentKind :: Venv ) )
124+ . executable ( Some ( executable) )
125+ . prefix ( Some ( prefix) )
126+ . version ( Some ( version. to_string ( ) ) )
127+ . symlinks ( Some ( symlinks) )
128+ . build ( )
129+ }
130+
131+ #[ test]
132+ fn same_prefix_is_not_flagged ( ) {
133+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
134+ let prefix = dir. path ( ) . to_path_buf ( ) ;
135+ let exe = prefix. join ( "bin" ) . join ( "python" ) ;
136+
137+ let env = make_env ( exe. clone ( ) , prefix. clone ( ) , "3.12.7" , vec ! [ exe. clone( ) ] ) ;
138+ let resolved = make_env ( exe. clone ( ) , prefix, "3.12.7" , vec ! [ exe] ) ;
139+
140+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
141+ assert ! ( result. is_none( ) , "identical prefixes should not be flagged" ) ;
142+ }
143+
144+ #[ cfg( unix) ]
145+ #[ test]
146+ fn symlinked_prefix_is_not_flagged ( ) {
147+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
148+ let real_prefix = dir. path ( ) . join ( "versioned" ) . join ( "myvenv-1.0.51" ) ;
149+ std:: fs:: create_dir_all ( & real_prefix) . unwrap ( ) ;
150+ let symlink_prefix = dir. path ( ) . join ( "myvenv" ) ;
151+ std:: os:: unix:: fs:: symlink ( & real_prefix, & symlink_prefix) . unwrap ( ) ;
152+
153+ let exe = symlink_prefix. join ( "bin" ) . join ( "python" ) ;
154+
155+ // Discovery sees the symlink path as prefix
156+ let env = make_env ( exe. clone ( ) , symlink_prefix, "3.12.7" , vec ! [ exe. clone( ) ] ) ;
157+ // Resolution (spawning Python) returns the canonical path
158+ let resolved = make_env ( exe. clone ( ) , real_prefix, "3.12.7" , vec ! [ exe] ) ;
159+
160+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
161+ assert ! (
162+ result. is_none( ) ,
163+ "symlinked prefix to the same directory should not be flagged"
164+ ) ;
165+ }
166+
167+ #[ test]
168+ fn different_prefix_is_flagged ( ) {
169+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
170+ let prefix_a = dir. path ( ) . join ( "env_a" ) ;
171+ let prefix_b = dir. path ( ) . join ( "env_b" ) ;
172+ std:: fs:: create_dir_all ( & prefix_a) . unwrap ( ) ;
173+ std:: fs:: create_dir_all ( & prefix_b) . unwrap ( ) ;
174+
175+ let exe = prefix_a. join ( "bin" ) . join ( "python" ) ;
176+
177+ let env = make_env ( exe. clone ( ) , prefix_a, "3.12.7" , vec ! [ exe. clone( ) ] ) ;
178+ let resolved = make_env ( exe. clone ( ) , prefix_b, "3.12.7" , vec ! [ exe] ) ;
179+
180+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
181+ let event = result. expect ( "genuinely different prefixes should be flagged" ) ;
182+ assert_eq ! ( event. invalid_prefix, Some ( true ) ) ;
183+ }
184+
185+ #[ test]
186+ fn none_prefix_is_not_flagged ( ) {
187+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
188+ let prefix = dir. path ( ) . to_path_buf ( ) ;
189+ let exe = prefix. join ( "bin" ) . join ( "python" ) ;
190+
191+ // env has no prefix
192+ let env = PythonEnvironmentBuilder :: new ( Some ( PythonEnvironmentKind :: Venv ) )
193+ . executable ( Some ( exe. clone ( ) ) )
194+ . version ( Some ( "3.12.7" . to_string ( ) ) )
195+ . symlinks ( Some ( vec ! [ exe. clone( ) ] ) )
196+ . build ( ) ;
197+ let resolved = make_env ( exe. clone ( ) , prefix, "3.12.7" , vec ! [ exe] ) ;
198+
199+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
200+ assert ! (
201+ result. is_none( ) ,
202+ "None prefix should not cause any inaccuracy flag"
203+ ) ;
204+ }
205+ }
0 commit comments