Skip to content

Commit 50a2ffd

Browse files
authored
Merge pull request #1057 from rd4398/disable-constraints
fix(bootstrap): auto-disable constraints and extend e2e test coverage
2 parents c1d79b5 + 9b4d820 commit 50a2ffd

3 files changed

Lines changed: 173 additions & 5 deletions

File tree

e2e/test_bootstrap_multiple_versions.sh

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,21 @@
77
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
88
source "$SCRIPTDIR/common.sh"
99

10-
# Create constraints file to pin build dependencies (keeps CI fast)
10+
# Create constraints file with generous ranges to test multiple versions
11+
# of build dependencies (not just top-level packages)
1112
constraints_file=$(mktemp)
1213
trap "rm -f $constraints_file" EXIT
1314
cat > "$constraints_file" <<EOF
14-
flit-core==3.11.0
15+
# Allow a range of flit-core versions to verify multiple-versions works for dependencies
16+
flit-core>=3.9,<3.12
1517
EOF
1618

1719
# Use tomli with a version range that matches exactly 3 versions (2.0.0, 2.0.1, 2.0.2)
1820
# tomli has no runtime dependencies, making it fast to bootstrap
19-
# It uses flit-core as build backend (pinned above)
21+
# It uses flit-core as build backend, and we allow multiple flit-core versions
22+
# to test that --multiple-versions works for the entire dependency chain
2023
# Using <=2.0.2 instead of <2.1 to be deterministic (tomli 2.1.0 exists)
21-
# Note: constraints file generation will fail (expected with multiple versions)
24+
# Note: constraints file generation is automatically disabled with --multiple-versions
2225
fromager \
2326
--log-file="$OUTDIR/bootstrap.log" \
2427
--error-log-file="$OUTDIR/fromager-errors.log" \
@@ -28,7 +31,7 @@ fromager \
2831
--constraints-file="$constraints_file" \
2932
bootstrap \
3033
--multiple-versions \
31-
'tomli>=2.0,<=2.0.2' || true
34+
'tomli>=2.0,<=2.0.2'
3235

3336
# Check that wheels were built
3437
echo "Checking for wheels..."
@@ -60,3 +63,22 @@ fi
6063

6164
echo ""
6265
echo "SUCCESS: All expected tomli versions (2.0.0, 2.0.1, 2.0.2) were bootstrapped"
66+
67+
# Verify that multiple versions of flit-core were built (dependency of tomli)
68+
# This confirms that --multiple-versions works for the entire dependency chain
69+
echo ""
70+
echo "Checking for flit-core versions (build dependency)..."
71+
FLIT_CORE_COUNT=$(find "$OUTDIR/wheels-repo/downloads/" -name 'flit_core-3.*.whl' | wc -l)
72+
echo "Found $FLIT_CORE_COUNT flit-core 3.x wheel(s)"
73+
74+
if [ "$FLIT_CORE_COUNT" -lt 2 ]; then
75+
echo ""
76+
echo "ERROR: Expected at least 2 flit-core versions, found $FLIT_CORE_COUNT"
77+
echo "The --multiple-versions flag should bootstrap multiple versions of dependencies too"
78+
echo ""
79+
echo "Found flit-core wheels:"
80+
find "$OUTDIR/wheels-repo/downloads/" -name 'flit_core-*.whl'
81+
exit 1
82+
fi
83+
84+
echo "✓ Multiple versions of flit-core were bootstrapped (confirms dependency chain handling)"

src/fromager/commands/bootstrap.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ def bootstrap(
159159
logger.info(
160160
"multiple versions mode enabled: will bootstrap all matching versions"
161161
)
162+
# Automatically disable constraints when multiple versions mode is enabled
163+
# because constraints.txt cannot handle multiple versions of the same package
164+
if not skip_constraints:
165+
logger.info(
166+
"automatically disabling constraints generation "
167+
"(incompatible with --multiple-versions)"
168+
)
169+
skip_constraints = True
162170

163171
pre_built = wkctx.settings.list_pre_built()
164172
if pre_built:

tests/test_bootstrap.py

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import io
2+
import logging
23
import pathlib
34
import textwrap
45
from unittest.mock import Mock, patch
@@ -541,6 +542,143 @@ def test_skip_constraints_cli_option() -> None:
541542
assert "Skip generating constraints.txt file" in result.output
542543

543544

545+
@patch("fromager.commands.bootstrap.bootstrapper.Bootstrapper")
546+
@patch("fromager.commands.bootstrap.server.start_wheel_server")
547+
@patch("fromager.commands.bootstrap.progress.progress_context")
548+
@patch("fromager.commands.bootstrap.metrics.summarize")
549+
def test_multiple_versions_auto_disables_constraints(
550+
mock_metrics: Mock,
551+
mock_progress: Mock,
552+
mock_server: Mock,
553+
mock_bootstrapper: Mock,
554+
tmp_context: context.WorkContext,
555+
caplog: pytest.LogCaptureFixture,
556+
) -> None:
557+
"""Test that --multiple-versions alone auto-disables constraints and logs message"""
558+
# Setup mocks
559+
mock_progress.return_value.__enter__.return_value = Mock()
560+
mock_progress.return_value.__exit__.return_value = None
561+
mock_bt_instance = Mock()
562+
mock_bt_instance.resolve_and_add_top_level.return_value = ("url", Version("1.0"))
563+
mock_bt_instance.finalize.return_value = 0
564+
mock_bootstrapper.return_value = mock_bt_instance
565+
566+
runner = CliRunner()
567+
with runner.isolated_filesystem():
568+
# Create a temporary requirements file
569+
pathlib.Path("req.txt").write_text("setuptools>=60\n")
570+
571+
# Invoke with --multiple-versions but NOT --skip-constraints
572+
with caplog.at_level(logging.INFO):
573+
result = runner.invoke(
574+
bootstrap.bootstrap,
575+
[
576+
"-r",
577+
"req.txt",
578+
"--multiple-versions",
579+
],
580+
obj=tmp_context,
581+
)
582+
583+
# Should succeed
584+
assert result.exit_code == 0
585+
586+
# Should log that constraints are auto-disabled
587+
assert "automatically disabling constraints generation" in caplog.text
588+
assert "incompatible with --multiple-versions" in caplog.text
589+
590+
591+
@patch("fromager.commands.bootstrap.bootstrapper.Bootstrapper")
592+
@patch("fromager.commands.bootstrap.server.start_wheel_server")
593+
@patch("fromager.commands.bootstrap.progress.progress_context")
594+
@patch("fromager.commands.bootstrap.metrics.summarize")
595+
def test_multiple_versions_with_skip_constraints_no_duplicate_log(
596+
mock_metrics: Mock,
597+
mock_progress: Mock,
598+
mock_server: Mock,
599+
mock_bootstrapper: Mock,
600+
tmp_context: context.WorkContext,
601+
caplog: pytest.LogCaptureFixture,
602+
) -> None:
603+
"""Test that --multiple-versions --skip-constraints together doesn't log auto-disable message"""
604+
# Setup mocks
605+
mock_progress.return_value.__enter__.return_value = Mock()
606+
mock_progress.return_value.__exit__.return_value = None
607+
mock_bt_instance = Mock()
608+
mock_bt_instance.resolve_and_add_top_level.return_value = ("url", Version("1.0"))
609+
mock_bt_instance.finalize.return_value = 0
610+
mock_bootstrapper.return_value = mock_bt_instance
611+
612+
runner = CliRunner()
613+
with runner.isolated_filesystem():
614+
# Create a temporary requirements file
615+
pathlib.Path("req.txt").write_text("setuptools>=60\n")
616+
617+
# Invoke with BOTH --multiple-versions AND --skip-constraints
618+
with caplog.at_level(logging.INFO):
619+
result = runner.invoke(
620+
bootstrap.bootstrap,
621+
[
622+
"-r",
623+
"req.txt",
624+
"--multiple-versions",
625+
"--skip-constraints",
626+
],
627+
obj=tmp_context,
628+
)
629+
630+
# Should succeed
631+
assert result.exit_code == 0
632+
633+
# Should NOT log the auto-disable message (already disabled by user)
634+
assert "automatically disabling constraints generation" not in caplog.text
635+
636+
637+
@patch("fromager.commands.bootstrap.bootstrapper.Bootstrapper")
638+
@patch("fromager.commands.bootstrap.server.start_wheel_server")
639+
@patch("fromager.commands.bootstrap.progress.progress_context")
640+
@patch("fromager.commands.bootstrap.metrics.summarize")
641+
@patch("fromager.commands.bootstrap.write_constraints_file")
642+
def test_without_multiple_versions_constraints_not_disabled(
643+
mock_write_constraints: Mock,
644+
mock_metrics: Mock,
645+
mock_progress: Mock,
646+
mock_server: Mock,
647+
mock_bootstrapper: Mock,
648+
tmp_context: context.WorkContext,
649+
) -> None:
650+
"""Test that without --multiple-versions, constraints are not auto-disabled"""
651+
# Setup mocks
652+
mock_progress.return_value.__enter__.return_value = Mock()
653+
mock_progress.return_value.__exit__.return_value = None
654+
mock_bt_instance = Mock()
655+
mock_bt_instance.resolve_and_add_top_level.return_value = ("url", Version("1.0"))
656+
mock_bt_instance.finalize.return_value = 0
657+
mock_bootstrapper.return_value = mock_bt_instance
658+
mock_write_constraints.return_value = True
659+
660+
runner = CliRunner()
661+
with runner.isolated_filesystem():
662+
# Create a temporary requirements file
663+
pathlib.Path("req.txt").write_text("setuptools>=60\n")
664+
665+
# Invoke WITHOUT --multiple-versions
666+
result = runner.invoke(
667+
bootstrap.bootstrap,
668+
[
669+
"-r",
670+
"req.txt",
671+
],
672+
obj=tmp_context,
673+
)
674+
675+
# Should succeed
676+
assert result.exit_code == 0
677+
678+
# write_constraints_file should have been called (constraints NOT disabled)
679+
assert mock_write_constraints.called
680+
681+
544682
@patch("fromager.gitutils.git_clone")
545683
def test_resolve_version_from_git_url_with_submodules_enabled(
546684
mock_git_clone: Mock,

0 commit comments

Comments
 (0)