Skip to content

Commit cdeb286

Browse files
shifa-khanclaude
andcommitted
test(e2e): add test-mode failure handling tests
Add e2e tests for --test-mode bootstrapping failures as requested in issue Tests cover: - Top-level resolution failure (non-existent package) - Secondary dependency resolution failure (constrained pbr) - Build failure without prebuilt fallback (local git fixture) - Build failure with prebuilt fallback (broken patch on setuptools) Co-Authored-By: Claude <claude@anthropic.com> Closes: #895
1 parent aec9c9c commit cdeb286

8 files changed

Lines changed: 416 additions & 0 deletions

File tree

e2e/ci_bootstrap_suite.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ run_test "bootstrap_prerelease"
2626
run_test "bootstrap_cache"
2727
run_test "bootstrap_sdist_only"
2828

29+
test_section "bootstrap test-mode tests"
30+
run_test "mode_resolution"
31+
run_test "mode_deps"
32+
run_test "mode_build"
33+
run_test "mode_fallback"
34+
2935
test_section "bootstrap git URL tests"
3036
run_test "bootstrap_git_url"
3137
run_test "bootstrap_git_url_tag"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[build-system]
2+
requires = ["setuptools"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "test_build_failure"
7+
version = "1.0.0"
8+
description = "Test fixture that intentionally fails to build"

e2e/test_build_failure/setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Setup script that intentionally fails during build."""
2+
import sys
3+
4+
# Fail immediately when this module is imported during build
5+
raise RuntimeError("Intentional build failure for e2e testing")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# This file intentionally raises an error when imported
2+
# to cause build failures for e2e testing
3+
raise RuntimeError("Intentional build failure for e2e testing")

e2e/test_mode_build.sh

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
#!/bin/bash
2+
# -*- indent-tabs-mode: nil; tab-width: 2; sh-indentation: 2; -*-
3+
4+
# Test --test-mode: build failure without prebuilt fallback
5+
#
6+
# Verifies that when a package fails to build and no prebuilt wheel is available
7+
# (because the package is not on PyPI), test-mode records the failure.
8+
# Uses a local git repo fixture with a broken build backend.
9+
#
10+
# See: https://github.com/python-wheel-build/fromager/issues/895
11+
12+
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
13+
source "$SCRIPTDIR/common.sh"
14+
15+
# Use the test_build_failure fixture (local git repo)
16+
# Initialize git repo at runtime (fixture files are committed without .git)
17+
FIXTURE_DIR="$SCRIPTDIR/test_build_failure"
18+
if [ ! -d "$FIXTURE_DIR/.git" ]; then
19+
(cd "$FIXTURE_DIR" && git init -q && \
20+
git config user.email "test@example.com" && \
21+
git config user.name "Test User" && \
22+
git add -A && git commit -q -m "init")
23+
fi
24+
FIXTURE_URL="git+file://${FIXTURE_DIR}"
25+
26+
# Create a requirements file pointing to the local fixture
27+
REQUIREMENTS_FILE="$OUTDIR/test-requirements.txt"
28+
echo "test_build_failure @ ${FIXTURE_URL}" > "$REQUIREMENTS_FILE"
29+
30+
# Run bootstrap in test mode
31+
# - Package resolves from local git repo
32+
# - Build fails (broken build backend)
33+
# - Prebuilt fallback fails (package not on PyPI)
34+
# - Failure should be recorded
35+
set +e
36+
fromager \
37+
--log-file="$OUTDIR/bootstrap.log" \
38+
--error-log-file="$OUTDIR/fromager-errors.log" \
39+
--sdists-repo="$OUTDIR/sdists-repo" \
40+
--wheels-repo="$OUTDIR/wheels-repo" \
41+
--work-dir="$OUTDIR/work-dir" \
42+
bootstrap --test-mode -r "$REQUIREMENTS_FILE"
43+
EXIT_CODE=$?
44+
set -e
45+
46+
pass=true
47+
48+
# Check 1: Exit code should be 1 (failures recorded)
49+
if [ "$EXIT_CODE" -ne 1 ]; then
50+
echo "FAIL: Expected exit code 1, got $EXIT_CODE" 1>&2
51+
pass=false
52+
fi
53+
54+
# Check 2: The test-mode-failures JSON file should exist
55+
FAILURES_FILE=$(find "$OUTDIR/work-dir" -name "test-mode-failures-*.json" 2>/dev/null | head -1)
56+
if [ -z "$FAILURES_FILE" ] || [ ! -f "$FAILURES_FILE" ]; then
57+
echo "FAIL: test-mode-failures-*.json file not found" 1>&2
58+
pass=false
59+
else
60+
echo "Found failures file: $FAILURES_FILE"
61+
62+
# Check 3: test_build_failure should be in failed packages
63+
# Note: package name uses underscore as recorded by fromager
64+
if ! jq -e '.failures[] | select(.package == "test_build_failure")' "$FAILURES_FILE" > /dev/null 2>&1; then
65+
echo "FAIL: Expected 'test_build_failure' in failed packages" 1>&2
66+
jq '.' "$FAILURES_FILE" 1>&2
67+
pass=false
68+
fi
69+
70+
# Check 4: failure_type should be "bootstrap" or "resolution" (both are valid build failures)
71+
FAILURE_TYPE=$(jq -r '[.failures[] | select(.package == "test_build_failure")][0].failure_type' "$FAILURES_FILE")
72+
if [ "$FAILURE_TYPE" != "bootstrap" ] && [ "$FAILURE_TYPE" != "resolution" ]; then
73+
echo "FAIL: Expected failure_type 'bootstrap' or 'resolution', got '$FAILURE_TYPE'" 1>&2
74+
pass=false
75+
fi
76+
77+
# Check 5: exception_message should mention the intentional failure or a build-related error
78+
EXCEPTION_MSG=$(jq -r '[.failures[] | select(.package == "test_build_failure")][0].exception_message' "$FAILURES_FILE")
79+
if [[ "$EXCEPTION_MSG" != *"Intentional build failure"* ]] && [[ "$EXCEPTION_MSG" != *"RuntimeError"* ]] && [[ "$EXCEPTION_MSG" != *"build"* ]]; then
80+
echo "FAIL: Expected exception message about build failure, got: $EXCEPTION_MSG" 1>&2
81+
pass=false
82+
fi
83+
fi
84+
85+
# Check 6: Log should show test mode enabled
86+
if ! grep -q "test mode enabled" "$OUTDIR/bootstrap.log"; then
87+
echo "FAIL: Log should contain 'test mode enabled'" 1>&2
88+
pass=false
89+
fi
90+
91+
# Check 7: Log should show fallback was attempted and failed
92+
if grep -q "pre-built fallback" "$OUTDIR/bootstrap.log"; then
93+
echo "INFO: Fallback was attempted (expected since package not on PyPI)"
94+
else
95+
echo "INFO: No fallback mention in log (may vary by code path)"
96+
fi
97+
98+
$pass

e2e/test_mode_deps.sh

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/bin/bash
2+
# -*- indent-tabs-mode: nil; tab-width: 2; sh-indentation: 2; -*-
3+
4+
# Test --test-mode: secondary dependency resolution failure
5+
#
6+
# Verifies that when a top-level package resolves but one of its dependencies
7+
# cannot be resolved, test-mode records the failure and continues processing.
8+
#
9+
# See: https://github.com/python-wheel-build/fromager/issues/895
10+
11+
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
12+
source "$SCRIPTDIR/common.sh"
13+
14+
# Use stevedore which depends on pbr
15+
# Constrain pbr to a version that doesn't exist to trigger secondary dep failure
16+
TOPLEVEL_PKG="stevedore==5.2.0"
17+
NONEXISTENT_VERSION="99999.0.0"
18+
19+
# Create a constraints file that forces pbr to a non-existent version
20+
CONSTRAINTS_FILE="$OUTDIR/test-constraints.txt"
21+
echo "pbr==${NONEXISTENT_VERSION}" > "$CONSTRAINTS_FILE"
22+
23+
# Run bootstrap in test mode
24+
# The top-level stevedore should resolve, but pbr should fail
25+
set +e
26+
fromager \
27+
--log-file="$OUTDIR/bootstrap.log" \
28+
--error-log-file="$OUTDIR/fromager-errors.log" \
29+
--sdists-repo="$OUTDIR/sdists-repo" \
30+
--wheels-repo="$OUTDIR/wheels-repo" \
31+
--work-dir="$OUTDIR/work-dir" \
32+
--constraints-file="$CONSTRAINTS_FILE" \
33+
bootstrap --test-mode "${TOPLEVEL_PKG}"
34+
EXIT_CODE=$?
35+
set -e
36+
37+
pass=true
38+
39+
# Check 1: Exit code should be 1 (indicating failures in test mode)
40+
if [ "$EXIT_CODE" -ne 1 ]; then
41+
echo "FAIL: Expected exit code 1, got $EXIT_CODE" 1>&2
42+
pass=false
43+
fi
44+
45+
# Check 2: The test-mode-failures JSON file should exist
46+
FAILURES_FILE=$(find "$OUTDIR/work-dir" -name "test-mode-failures-*.json" 2>/dev/null | head -1)
47+
if [ -z "$FAILURES_FILE" ] || [ ! -f "$FAILURES_FILE" ]; then
48+
echo "FAIL: test-mode-failures-*.json file not found in $OUTDIR/work-dir" 1>&2
49+
ls -la "$OUTDIR/work-dir" 1>&2
50+
pass=false
51+
else
52+
echo "Found failures file: $FAILURES_FILE"
53+
54+
# Check 3: JSON file should contain at least one failure
55+
FAILURE_COUNT=$(jq '.failures | length' "$FAILURES_FILE")
56+
if [ "$FAILURE_COUNT" -lt 1 ]; then
57+
echo "FAIL: Expected at least 1 failure in JSON, got $FAILURE_COUNT" 1>&2
58+
jq '.' "$FAILURES_FILE" 1>&2
59+
pass=false
60+
fi
61+
62+
# Check 4: pbr should be in the failed packages (secondary dependency)
63+
if ! jq -e '.failures[] | select(.package == "pbr")' "$FAILURES_FILE" > /dev/null 2>&1; then
64+
echo "FAIL: Expected 'pbr' to be in failed packages" 1>&2
65+
jq '.' "$FAILURES_FILE" 1>&2
66+
pass=false
67+
fi
68+
69+
# Check 5: All pbr failures should be "resolution" type
70+
# Use first match since pbr may fail multiple times (as build dep of multiple packages)
71+
PBR_FAILURE_TYPE=$(jq -r '[.failures[] | select(.package == "pbr")][0].failure_type' "$FAILURES_FILE")
72+
if [ "$PBR_FAILURE_TYPE" != "resolution" ]; then
73+
echo "FAIL: Expected failure_type 'resolution' for pbr, got '$PBR_FAILURE_TYPE'" 1>&2
74+
jq '.' "$FAILURES_FILE" 1>&2
75+
pass=false
76+
fi
77+
fi
78+
79+
# Check 6: Log should contain test mode messages
80+
if ! grep -q "test mode enabled" "$OUTDIR/bootstrap.log"; then
81+
echo "FAIL: Log should contain 'test mode enabled' message" 1>&2
82+
pass=false
83+
fi
84+
85+
# Check 7: stevedore should have been resolved (top-level success)
86+
if ! grep -q "stevedore.*resolves to" "$OUTDIR/bootstrap.log"; then
87+
echo "FAIL: stevedore should have been resolved" 1>&2
88+
pass=false
89+
fi
90+
91+
$pass

e2e/test_mode_fallback.sh

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
#!/bin/bash
2+
# -*- indent-tabs-mode: nil; tab-width: 2; sh-indentation: 2; -*-
3+
4+
# Test --test-mode: build failure with prebuilt fallback
5+
#
6+
# Verifies that when a source build fails but a prebuilt wheel is available,
7+
# test-mode uses the prebuilt wheel as fallback and continues without failure.
8+
# Uses a broken patch to trigger the build failure, then falls back to PyPI wheel.
9+
#
10+
# See: https://github.com/python-wheel-build/fromager/issues/895
11+
12+
SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
13+
source "$SCRIPTDIR/common.sh"
14+
15+
# Use setuptools - it's on PyPI with prebuilt wheels
16+
DIST="setuptools"
17+
VERSION="75.8.0"
18+
19+
# Step 1: Configure settings to mark setuptools as NOT prebuilt
20+
# This forces fromager to try building from source
21+
SETTINGS_DIR="$OUTDIR/test-settings"
22+
mkdir -p "$SETTINGS_DIR"
23+
cat > "$SETTINGS_DIR/${DIST}.yaml" << EOF
24+
variants:
25+
cpu:
26+
pre_built: false
27+
EOF
28+
29+
# Step 2: Create a broken patches dir that will cause build to fail
30+
# We create a patch targeting setup.py with wrong content - patch will fail
31+
# without prompting for input (unlike targeting a non-existent file)
32+
PATCHES_DIR="$OUTDIR/test-patches"
33+
mkdir -p "$PATCHES_DIR/${DIST}"
34+
cat > "$PATCHES_DIR/${DIST}/break-build.patch" << 'PATCHEOF'
35+
--- a/setup.py
36+
+++ b/setup.py
37+
@@ -1,3 +1,3 @@
38+
-this content does not match
39+
-the actual setup.py file
40+
-so patch will fail
41+
+replaced content
42+
+that will never
43+
+be applied
44+
PATCHEOF
45+
46+
# Step 3: Run bootstrap in test mode
47+
# - Package will resolve from PyPI
48+
# - Source preparation will fail (bad patch)
49+
# - Prebuilt fallback should succeed (wheel on PyPI)
50+
echo "Running test-mode bootstrap with broken patch..."
51+
set +e
52+
fromager \
53+
--log-file="$OUTDIR/bootstrap.log" \
54+
--error-log-file="$OUTDIR/fromager-errors.log" \
55+
--sdists-repo="$OUTDIR/sdists-repo" \
56+
--wheels-repo="$OUTDIR/wheels-repo" \
57+
--work-dir="$OUTDIR/work-dir" \
58+
--settings-dir="$SETTINGS_DIR" \
59+
--patches-dir="$PATCHES_DIR" \
60+
bootstrap --test-mode "${DIST}==${VERSION}"
61+
EXIT_CODE=$?
62+
set -e
63+
64+
pass=true
65+
66+
# Check 1: Exit code should be 0 (fallback succeeded, no failures recorded)
67+
echo "Exit code: $EXIT_CODE"
68+
if [ "$EXIT_CODE" -ne 0 ]; then
69+
echo "FAIL: Expected exit code 0 (fallback success), got $EXIT_CODE" 1>&2
70+
pass=false
71+
fi
72+
73+
# Check 2: Log should show test mode was enabled
74+
if ! grep -q "test mode enabled" "$OUTDIR/bootstrap.log"; then
75+
echo "FAIL: Log should contain 'test mode enabled' message" 1>&2
76+
pass=false
77+
fi
78+
79+
# Check 3: Look for evidence of the patch failure
80+
if grep -q "applying patch\|patch" "$OUTDIR/bootstrap.log"; then
81+
echo "Patch application was attempted"
82+
fi
83+
84+
# Check 4: Prebuilt fallback MUST be triggered and succeed
85+
if ! grep -q "pre-built fallback" "$OUTDIR/bootstrap.log"; then
86+
echo "FAIL: Expected prebuilt fallback to be triggered" 1>&2
87+
pass=false
88+
elif ! grep -q "successfully used pre-built wheel" "$OUTDIR/bootstrap.log"; then
89+
echo "FAIL: Prebuilt fallback was triggered but did not succeed" 1>&2
90+
pass=false
91+
else
92+
echo "SUCCESS: Prebuilt fallback triggered and succeeded"
93+
fi
94+
95+
# Check 5: No failures should be recorded (fallback succeeded)
96+
FAILURES_FILE=$(find "$OUTDIR/work-dir" -name "test-mode-failures-*.json" 2>/dev/null | head -1)
97+
if [ -n "$FAILURES_FILE" ] && [ -f "$FAILURES_FILE" ]; then
98+
FAILURE_COUNT=$(jq '.failures | length' "$FAILURES_FILE")
99+
if [ "$FAILURE_COUNT" -gt 0 ]; then
100+
echo "FAIL: Expected no failures (fallback should succeed), got $FAILURE_COUNT" 1>&2
101+
jq '.failures[] | {package, failure_type, exception_type}' "$FAILURES_FILE" 1>&2
102+
pass=false
103+
fi
104+
fi
105+
106+
# Check 6: Verify test mode completed
107+
if grep -q "test mode:" "$OUTDIR/bootstrap.log"; then
108+
echo "Test mode processing completed"
109+
else
110+
echo "NOTE: Test mode summary not found in log" 1>&2
111+
fi
112+
113+
$pass

0 commit comments

Comments
 (0)