Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 27 additions & 5 deletions e2e/test_bootstrap_multiple_versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
source "$SCRIPTDIR/common.sh"

# Create constraints file to pin build dependencies (keeps CI fast)
# Create constraints file with generous ranges to test multiple versions
# of build dependencies (not just top-level packages)
constraints_file=$(mktemp)
trap "rm -f $constraints_file" EXIT
Comment thread
rd4398 marked this conversation as resolved.
cat > "$constraints_file" <<EOF
flit-core==3.11.0
# Allow a range of flit-core versions to verify multiple-versions works for dependencies
flit-core>=3.9,<3.12
EOF

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

# Check that wheels were built
echo "Checking for wheels..."
Expand Down Expand Up @@ -60,3 +63,22 @@ fi

echo ""
echo "SUCCESS: All expected tomli versions (2.0.0, 2.0.1, 2.0.2) were bootstrapped"

# Verify that multiple versions of flit-core were built (dependency of tomli)
# This confirms that --multiple-versions works for the entire dependency chain
echo ""
echo "Checking for flit-core versions (build dependency)..."
FLIT_CORE_COUNT=$(find "$OUTDIR/wheels-repo/downloads/" -name 'flit_core-3.*.whl' | wc -l)
echo "Found $FLIT_CORE_COUNT flit-core 3.x wheel(s)"

if [ "$FLIT_CORE_COUNT" -lt 2 ]; then
echo ""
echo "ERROR: Expected at least 2 flit-core versions, found $FLIT_CORE_COUNT"
echo "The --multiple-versions flag should bootstrap multiple versions of dependencies too"
echo ""
echo "Found flit-core wheels:"
find "$OUTDIR/wheels-repo/downloads/" -name 'flit_core-*.whl'
exit 1
fi
Comment thread
rd4398 marked this conversation as resolved.

echo "✓ Multiple versions of flit-core were bootstrapped (confirms dependency chain handling)"
8 changes: 8 additions & 0 deletions src/fromager/commands/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,14 @@ def bootstrap(
logger.info(
"multiple versions mode enabled: will bootstrap all matching versions"
)
# Automatically disable constraints when multiple versions mode is enabled
# because constraints.txt cannot handle multiple versions of the same package
if not skip_constraints:
logger.info(
"automatically disabling constraints generation "
"(incompatible with --multiple-versions)"
)
skip_constraints = True
Comment thread
LalatenduMohanty marked this conversation as resolved.

pre_built = wkctx.settings.list_pre_built()
if pre_built:
Expand Down
138 changes: 138 additions & 0 deletions tests/test_bootstrap.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io
import logging
import pathlib
import textwrap
from unittest.mock import Mock, patch
Expand Down Expand Up @@ -541,6 +542,143 @@ def test_skip_constraints_cli_option() -> None:
assert "Skip generating constraints.txt file" in result.output


@patch("fromager.commands.bootstrap.bootstrapper.Bootstrapper")
@patch("fromager.commands.bootstrap.server.start_wheel_server")
@patch("fromager.commands.bootstrap.progress.progress_context")
@patch("fromager.commands.bootstrap.metrics.summarize")
def test_multiple_versions_auto_disables_constraints(
mock_metrics: Mock,
mock_progress: Mock,
mock_server: Mock,
mock_bootstrapper: Mock,
tmp_context: context.WorkContext,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that --multiple-versions alone auto-disables constraints and logs message"""
# Setup mocks
mock_progress.return_value.__enter__.return_value = Mock()
mock_progress.return_value.__exit__.return_value = None
mock_bt_instance = Mock()
mock_bt_instance.resolve_and_add_top_level.return_value = ("url", Version("1.0"))
mock_bt_instance.finalize.return_value = 0
mock_bootstrapper.return_value = mock_bt_instance

runner = CliRunner()
with runner.isolated_filesystem():
# Create a temporary requirements file
pathlib.Path("req.txt").write_text("setuptools>=60\n")

# Invoke with --multiple-versions but NOT --skip-constraints
with caplog.at_level(logging.INFO):
result = runner.invoke(
bootstrap.bootstrap,
[
"-r",
"req.txt",
"--multiple-versions",
],
obj=tmp_context,
)

# Should succeed
assert result.exit_code == 0

# Should log that constraints are auto-disabled
assert "automatically disabling constraints generation" in caplog.text
assert "incompatible with --multiple-versions" in caplog.text


@patch("fromager.commands.bootstrap.bootstrapper.Bootstrapper")
@patch("fromager.commands.bootstrap.server.start_wheel_server")
@patch("fromager.commands.bootstrap.progress.progress_context")
@patch("fromager.commands.bootstrap.metrics.summarize")
def test_multiple_versions_with_skip_constraints_no_duplicate_log(
mock_metrics: Mock,
mock_progress: Mock,
mock_server: Mock,
mock_bootstrapper: Mock,
tmp_context: context.WorkContext,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that --multiple-versions --skip-constraints together doesn't log auto-disable message"""
# Setup mocks
mock_progress.return_value.__enter__.return_value = Mock()
mock_progress.return_value.__exit__.return_value = None
mock_bt_instance = Mock()
mock_bt_instance.resolve_and_add_top_level.return_value = ("url", Version("1.0"))
mock_bt_instance.finalize.return_value = 0
mock_bootstrapper.return_value = mock_bt_instance

runner = CliRunner()
with runner.isolated_filesystem():
# Create a temporary requirements file
pathlib.Path("req.txt").write_text("setuptools>=60\n")

# Invoke with BOTH --multiple-versions AND --skip-constraints
with caplog.at_level(logging.INFO):
result = runner.invoke(
bootstrap.bootstrap,
[
"-r",
"req.txt",
"--multiple-versions",
"--skip-constraints",
],
obj=tmp_context,
)

# Should succeed
assert result.exit_code == 0

# Should NOT log the auto-disable message (already disabled by user)
assert "automatically disabling constraints generation" not in caplog.text


@patch("fromager.commands.bootstrap.bootstrapper.Bootstrapper")
@patch("fromager.commands.bootstrap.server.start_wheel_server")
@patch("fromager.commands.bootstrap.progress.progress_context")
@patch("fromager.commands.bootstrap.metrics.summarize")
@patch("fromager.commands.bootstrap.write_constraints_file")
def test_without_multiple_versions_constraints_not_disabled(
mock_write_constraints: Mock,
mock_metrics: Mock,
mock_progress: Mock,
mock_server: Mock,
mock_bootstrapper: Mock,
tmp_context: context.WorkContext,
) -> None:
"""Test that without --multiple-versions, constraints are not auto-disabled"""
# Setup mocks
mock_progress.return_value.__enter__.return_value = Mock()
mock_progress.return_value.__exit__.return_value = None
mock_bt_instance = Mock()
mock_bt_instance.resolve_and_add_top_level.return_value = ("url", Version("1.0"))
mock_bt_instance.finalize.return_value = 0
mock_bootstrapper.return_value = mock_bt_instance
mock_write_constraints.return_value = True

runner = CliRunner()
with runner.isolated_filesystem():
# Create a temporary requirements file
pathlib.Path("req.txt").write_text("setuptools>=60\n")

# Invoke WITHOUT --multiple-versions
result = runner.invoke(
bootstrap.bootstrap,
[
"-r",
"req.txt",
],
obj=tmp_context,
)

# Should succeed
assert result.exit_code == 0

# write_constraints_file should have been called (constraints NOT disabled)
assert mock_write_constraints.called


@patch("fromager.gitutils.git_clone")
def test_resolve_version_from_git_url_with_submodules_enabled(
mock_git_clone: Mock,
Expand Down
Loading