@@ -202,4 +202,180 @@ mod tests {
202202 "None prefix should not cause any inaccuracy flag"
203203 ) ;
204204 }
205+
206+ // ── are_versions_different ────────────────────────────────────
207+
208+ #[ test]
209+ fn same_version_is_not_different ( ) {
210+ assert_eq ! ( are_versions_different( "3.12.7" , "3.12.7" ) , Some ( false ) ) ;
211+ }
212+
213+ #[ test]
214+ fn different_patch_version_is_detected ( ) {
215+ assert_eq ! ( are_versions_different( "3.12.7" , "3.12.6" ) , Some ( true ) ) ;
216+ }
217+
218+ #[ test]
219+ fn different_minor_version_is_detected ( ) {
220+ assert_eq ! ( are_versions_different( "3.13.0" , "3.12.7" ) , Some ( true ) ) ;
221+ }
222+
223+ #[ test]
224+ fn version_with_suffix_compares_only_numeric_part ( ) {
225+ // "3.12.7+" or "3.12.7rc1" — the regex extracts only the digits
226+ assert_eq ! ( are_versions_different( "3.12.7rc1" , "3.12.7" ) , Some ( false ) ) ;
227+ }
228+
229+ #[ test]
230+ fn non_version_strings_return_none ( ) {
231+ assert_eq ! ( are_versions_different( "not-a-version" , "3.12.7" ) , None ) ;
232+ assert_eq ! ( are_versions_different( "3.12.7" , "not-a-version" ) , None ) ;
233+ }
234+
235+ #[ test]
236+ fn empty_expected_version_returns_none ( ) {
237+ assert_eq ! ( are_versions_different( "3.12.7" , "" ) , None ) ;
238+ }
239+
240+ // ── executable mismatch ───────────────────────────────────────
241+
242+ #[ test]
243+ fn different_executable_is_flagged ( ) {
244+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
245+ let prefix = dir. path ( ) . to_path_buf ( ) ;
246+ let exe_a = prefix. join ( "bin" ) . join ( "python3.12" ) ;
247+ let exe_b = prefix. join ( "bin" ) . join ( "python3" ) ;
248+
249+ let env = make_env ( exe_a, prefix. clone ( ) , "3.12.7" , vec ! [ ] ) ;
250+ let resolved = make_env ( exe_b, prefix, "3.12.7" , vec ! [ ] ) ;
251+
252+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
253+ let event = result. expect ( "different executables should be flagged" ) ;
254+ assert_eq ! ( event. invalid_executable, Some ( true ) ) ;
255+ }
256+
257+ #[ test]
258+ fn none_executable_is_not_flagged ( ) {
259+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
260+ let prefix = dir. path ( ) . to_path_buf ( ) ;
261+ let exe = prefix. join ( "bin" ) . join ( "python" ) ;
262+
263+ let env = PythonEnvironmentBuilder :: new ( Some ( PythonEnvironmentKind :: Venv ) )
264+ . prefix ( Some ( prefix. clone ( ) ) )
265+ . version ( Some ( "3.12.7" . to_string ( ) ) )
266+ . symlinks ( Some ( vec ! [ exe. clone( ) ] ) )
267+ . build ( ) ;
268+ let resolved = make_env ( exe. clone ( ) , prefix, "3.12.7" , vec ! [ exe] ) ;
269+
270+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
271+ assert ! (
272+ result. is_none( ) ,
273+ "None executable should not cause any inaccuracy flag"
274+ ) ;
275+ }
276+
277+ // ── executable not in symlinks ────────────────────────────────
278+
279+ #[ test]
280+ fn resolved_executable_not_in_symlinks_is_flagged ( ) {
281+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
282+ let prefix = dir. path ( ) . to_path_buf ( ) ;
283+ let exe = prefix. join ( "bin" ) . join ( "python" ) ;
284+ // Resolved executable differs from the one env knows about —
285+ // the builder auto-adds the env executable to symlinks, so the
286+ // resolved exe must be a genuinely different path to be "not in symlinks".
287+ let resolved_exe = prefix. join ( "bin" ) . join ( "python3.12" ) ;
288+
289+ let env = make_env ( exe, prefix. clone ( ) , "3.12.7" , vec ! [ ] ) ;
290+ let resolved = make_env ( resolved_exe, prefix, "3.12.7" , vec ! [ ] ) ;
291+
292+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
293+ let event = result. expect ( "resolved exe not in symlinks should be flagged" ) ;
294+ assert_eq ! ( event. executable_not_in_symlinks, Some ( true ) ) ;
295+ }
296+
297+ #[ test]
298+ fn resolved_executable_in_symlinks_is_not_flagged ( ) {
299+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
300+ let prefix = dir. path ( ) . to_path_buf ( ) ;
301+ let exe = prefix. join ( "bin" ) . join ( "python" ) ;
302+
303+ // env's symlinks include the resolved executable
304+ let env = make_env ( exe. clone ( ) , prefix. clone ( ) , "3.12.7" , vec ! [ exe. clone( ) ] ) ;
305+ let resolved = make_env ( exe. clone ( ) , prefix, "3.12.7" , vec ! [ exe] ) ;
306+
307+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
308+ assert ! ( result. is_none( ) ) ;
309+ }
310+
311+ // ── architecture mismatch ─────────────────────────────────────
312+
313+ #[ test]
314+ fn different_arch_is_flagged ( ) {
315+ use pet_core:: arch:: Architecture ;
316+
317+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
318+ let prefix = dir. path ( ) . to_path_buf ( ) ;
319+ let exe = prefix. join ( "bin" ) . join ( "python" ) ;
320+
321+ let env = PythonEnvironmentBuilder :: new ( Some ( PythonEnvironmentKind :: Venv ) )
322+ . executable ( Some ( exe. clone ( ) ) )
323+ . prefix ( Some ( prefix. clone ( ) ) )
324+ . version ( Some ( "3.12.7" . to_string ( ) ) )
325+ . symlinks ( Some ( vec ! [ exe. clone( ) ] ) )
326+ . arch ( Some ( Architecture :: X64 ) )
327+ . build ( ) ;
328+ let resolved = PythonEnvironmentBuilder :: new ( Some ( PythonEnvironmentKind :: Venv ) )
329+ . executable ( Some ( exe. clone ( ) ) )
330+ . prefix ( Some ( prefix) )
331+ . version ( Some ( "3.12.7" . to_string ( ) ) )
332+ . symlinks ( Some ( vec ! [ exe] ) )
333+ . arch ( Some ( Architecture :: X86 ) )
334+ . build ( ) ;
335+
336+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
337+ let event = result. expect ( "different architectures should be flagged" ) ;
338+ assert_eq ! ( event. invalid_arch, Some ( true ) ) ;
339+ }
340+
341+ #[ test]
342+ fn none_arch_is_not_flagged ( ) {
343+ use pet_core:: arch:: Architecture ;
344+
345+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
346+ let prefix = dir. path ( ) . to_path_buf ( ) ;
347+ let exe = prefix. join ( "bin" ) . join ( "python" ) ;
348+
349+ // env has no arch, resolved has X64
350+ let env = make_env ( exe. clone ( ) , prefix. clone ( ) , "3.12.7" , vec ! [ exe. clone( ) ] ) ;
351+ let resolved = PythonEnvironmentBuilder :: new ( Some ( PythonEnvironmentKind :: Venv ) )
352+ . executable ( Some ( exe. clone ( ) ) )
353+ . prefix ( Some ( prefix) )
354+ . version ( Some ( "3.12.7" . to_string ( ) ) )
355+ . symlinks ( Some ( vec ! [ exe] ) )
356+ . arch ( Some ( Architecture :: X64 ) )
357+ . build ( ) ;
358+
359+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
360+ assert ! (
361+ result. is_none( ) ,
362+ "None arch should not cause any inaccuracy flag"
363+ ) ;
364+ }
365+
366+ // ── version mismatch ──────────────────────────────────────────
367+
368+ #[ test]
369+ fn different_version_is_flagged ( ) {
370+ let dir = tempfile:: tempdir ( ) . unwrap ( ) ;
371+ let prefix = dir. path ( ) . to_path_buf ( ) ;
372+ let exe = prefix. join ( "bin" ) . join ( "python" ) ;
373+
374+ let env = make_env ( exe. clone ( ) , prefix. clone ( ) , "3.12.6" , vec ! [ exe. clone( ) ] ) ;
375+ let resolved = make_env ( exe. clone ( ) , prefix, "3.12.7" , vec ! [ exe] ) ;
376+
377+ let result = report_inaccuracies_identified_after_resolving ( & NoopReporter , & env, & resolved) ;
378+ let event = result. expect ( "different versions should be flagged" ) ;
379+ assert_eq ! ( event. invalid_version, Some ( true ) ) ;
380+ }
205381}
0 commit comments