Skip to content

Commit f31d17c

Browse files
test(test-mode): add tests for prebuilt fallback path in bootstrap()
Co-Authored-By: Claude <claude@anthropic.com> Signed-off-by: Lalatendu Mohanty <lmohanty@redhat.com>
1 parent 0e53faf commit f31d17c

File tree

1 file changed

+179
-0
lines changed

1 file changed

+179
-0
lines changed

tests/test_bootstrap_test_mode.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import pytest
1616
from packaging.requirements import Requirement
17+
from packaging.version import Version
1718

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

0 commit comments

Comments
 (0)