@@ -290,3 +290,191 @@ def test_resolution_failure_raises_in_normal_mode(
290290 ):
291291 with pytest .raises (RuntimeError , match = "Version resolution failed" ):
292292 bt .bootstrap (req = req , req_type = RequirementType .TOP_LEVEL )
293+
294+
295+ class TestPrebuiltFallback :
296+ """Test prebuilt fallback behavior in test mode when build fails."""
297+
298+ def test_fallback_succeeds_no_failure_recorded (
299+ self , tmp_context : context .WorkContext , caplog : pytest .LogCaptureFixture
300+ ) -> None :
301+ """Test that successful fallback to prebuilt doesn't record a failure."""
302+ from packaging .version import Version
303+
304+ bt = bootstrapper .Bootstrapper (ctx = tmp_context , test_mode = True )
305+ req = Requirement ("test-package>=1.0" )
306+
307+ with (
308+ mock .patch .object (
309+ bt ,
310+ "resolve_version" ,
311+ return_value = ("https://sdist.url" , Version ("1.0" )),
312+ ),
313+ mock .patch .object (bt , "_add_to_graph" ),
314+ mock .patch .object (bt , "_has_been_seen" , return_value = False ),
315+ mock .patch .object (bt , "_mark_as_seen" ),
316+ # First call fails (build), second succeeds (fallback with force_prebuilt)
317+ mock .patch .object (
318+ bt ,
319+ "_bootstrap_impl" ,
320+ side_effect = [RuntimeError ("Build failed" ), None ],
321+ ),
322+ mock .patch .object (
323+ bt ,
324+ "_resolve_prebuilt_with_history" ,
325+ return_value = ("https://wheel.url" , Version ("1.0" )),
326+ ),
327+ ):
328+ bt .bootstrap (req = req , req_type = RequirementType .TOP_LEVEL )
329+
330+ # No failure recorded because fallback succeeded
331+ assert len (bt .failed_packages ) == 0
332+ assert "successfully used pre-built wheel" in caplog .text
333+
334+ def test_fallback_version_mismatch_logs_warning (
335+ self , tmp_context : context .WorkContext , caplog : pytest .LogCaptureFixture
336+ ) -> None :
337+ """Test that version mismatch during fallback logs a warning."""
338+ from packaging .version import Version
339+
340+ bt = bootstrapper .Bootstrapper (ctx = tmp_context , test_mode = True )
341+ req = Requirement ("test-package>=1.0" )
342+
343+ with (
344+ mock .patch .object (
345+ bt ,
346+ "resolve_version" ,
347+ return_value = ("https://sdist.url" , Version ("1.0" )),
348+ ),
349+ mock .patch .object (bt , "_add_to_graph" ),
350+ mock .patch .object (bt , "_has_been_seen" , return_value = False ),
351+ mock .patch .object (bt , "_mark_as_seen" ),
352+ mock .patch .object (
353+ bt ,
354+ "_bootstrap_impl" ,
355+ side_effect = [RuntimeError ("Build failed" ), None ],
356+ ),
357+ # Fallback resolves to different version
358+ mock .patch .object (
359+ bt ,
360+ "_resolve_prebuilt_with_history" ,
361+ return_value = ("https://wheel.url" , Version ("1.1" )),
362+ ),
363+ ):
364+ bt .bootstrap (req = req , req_type = RequirementType .TOP_LEVEL )
365+
366+ # No failure recorded because fallback succeeded
367+ assert len (bt .failed_packages ) == 0
368+ assert "version mismatch" in caplog .text
369+ assert "requested 1.0" in caplog .text
370+ assert "fallback 1.1" in caplog .text
371+
372+ def test_fallback_also_fails_records_original_error (
373+ self , tmp_context : context .WorkContext , caplog : pytest .LogCaptureFixture
374+ ) -> None :
375+ """Test that when fallback also fails, original build error is recorded."""
376+ from packaging .version import Version
377+
378+ bt = bootstrapper .Bootstrapper (ctx = tmp_context , test_mode = True )
379+ req = Requirement ("test-package>=1.0" )
380+
381+ with (
382+ mock .patch .object (
383+ bt ,
384+ "resolve_version" ,
385+ return_value = ("https://sdist.url" , Version ("1.0" )),
386+ ),
387+ mock .patch .object (bt , "_add_to_graph" ),
388+ mock .patch .object (bt , "_has_been_seen" , return_value = False ),
389+ mock .patch .object (bt , "_mark_as_seen" ),
390+ # Both build and fallback fail
391+ mock .patch .object (
392+ bt ,
393+ "_bootstrap_impl" ,
394+ side_effect = [
395+ RuntimeError ("Original build failed" ),
396+ RuntimeError ("Fallback also failed" ),
397+ ],
398+ ),
399+ mock .patch .object (
400+ bt ,
401+ "_resolve_prebuilt_with_history" ,
402+ return_value = ("https://wheel.url" , Version ("1.0" )),
403+ ),
404+ ):
405+ bt .bootstrap (req = req , req_type = RequirementType .TOP_LEVEL )
406+
407+ # Failure recorded with ORIGINAL error, not fallback error
408+ assert len (bt .failed_packages ) == 1
409+ failure = bt .failed_packages [0 ]
410+ assert failure ["package" ] == "test-package"
411+ assert failure ["version" ] == "1.0"
412+ assert failure ["failure_type" ] == "bootstrap"
413+ assert "Original build failed" in failure ["exception_message" ]
414+ assert "pre-built fallback also failed" in caplog .text
415+
416+ def test_fallback_resolution_fails_records_original_error (
417+ self , tmp_context : context .WorkContext
418+ ) -> None :
419+ """Test that when prebuilt resolution fails, original build error is recorded."""
420+ from packaging .version import Version
421+
422+ bt = bootstrapper .Bootstrapper (ctx = tmp_context , test_mode = True )
423+ req = Requirement ("test-package>=1.0" )
424+
425+ with (
426+ mock .patch .object (
427+ bt ,
428+ "resolve_version" ,
429+ return_value = ("https://sdist.url" , Version ("1.0" )),
430+ ),
431+ mock .patch .object (bt , "_add_to_graph" ),
432+ mock .patch .object (bt , "_has_been_seen" , return_value = False ),
433+ mock .patch .object (bt , "_mark_as_seen" ),
434+ mock .patch .object (
435+ bt ,
436+ "_bootstrap_impl" ,
437+ side_effect = RuntimeError ("Original build failed" ),
438+ ),
439+ # Prebuilt resolution fails
440+ mock .patch .object (
441+ bt ,
442+ "_resolve_prebuilt_with_history" ,
443+ side_effect = RuntimeError ("No prebuilt available" ),
444+ ),
445+ ):
446+ bt .bootstrap (req = req , req_type = RequirementType .TOP_LEVEL )
447+
448+ # Failure recorded with ORIGINAL error
449+ assert len (bt .failed_packages ) == 1
450+ assert "Original build failed" in bt .failed_packages [0 ]["exception_message" ]
451+
452+ def test_build_failure_raises_in_normal_mode (
453+ self , tmp_context : context .WorkContext
454+ ) -> None :
455+ """Test that build failures raise immediately in normal mode (no fallback)."""
456+ from packaging .version import Version
457+
458+ bt = bootstrapper .Bootstrapper (ctx = tmp_context , test_mode = False )
459+ req = Requirement ("test-package>=1.0" )
460+
461+ with (
462+ mock .patch .object (
463+ bt ,
464+ "resolve_version" ,
465+ return_value = ("https://sdist.url" , Version ("1.0" )),
466+ ),
467+ mock .patch .object (bt , "_add_to_graph" ),
468+ mock .patch .object (bt , "_has_been_seen" , return_value = False ),
469+ mock .patch .object (bt , "_mark_as_seen" ),
470+ mock .patch .object (
471+ bt ,
472+ "_bootstrap_impl" ,
473+ side_effect = RuntimeError ("Build failed" ),
474+ ),
475+ ):
476+ with pytest .raises (RuntimeError , match = "Build failed" ):
477+ bt .bootstrap (req = req , req_type = RequirementType .TOP_LEVEL )
478+
479+ # No fallback attempted in normal mode
480+ assert len (bt .failed_packages ) == 0
0 commit comments