Skip to content

Commit 7cea778

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 Made-with: Cursor
1 parent e61cd02 commit 7cea778

File tree

8 files changed

+450
-0
lines changed

8 files changed

+450
-0
lines changed

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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# mypy: ignore-errors
2+
"""Setup script that intentionally fails during wheel build.
3+
4+
This fixture is designed to pass metadata extraction but fail during
5+
actual wheel building, producing a 'bootstrap' failure in test-mode.
6+
The failure is triggered by a custom build_ext command that always fails.
7+
"""
8+
9+
from setuptools import Extension, setup
10+
from setuptools.command.build_ext import build_ext
11+
12+
13+
class FailingBuildExt(build_ext):
14+
"""Custom build_ext that always fails."""
15+
16+
def run(self) -> None:
17+
raise RuntimeError("Intentional build failure for e2e testing")
18+
19+
20+
setup(
21+
ext_modules=[Extension("test_build_failure._dummy", sources=["missing.c"])],
22+
cmdclass={"build_ext": FailingBuildExt},
23+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Test fixture package for e2e build failure tests."""

e2e/test_mode_build.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 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+
CREATED_FIXTURE_GIT=false
19+
if [ ! -d "$FIXTURE_DIR/.git" ]; then
20+
CREATED_FIXTURE_GIT=true
21+
(cd "$FIXTURE_DIR" && git init -q && \
22+
git config user.email "test@example.com" && \
23+
git config user.name "Test User" && \
24+
git add -A && git commit -q -m "init")
25+
fi
26+
FIXTURE_URL="git+file://${FIXTURE_DIR}"
27+
28+
# Cleanup .git on exit if we created it (prevents flaky reruns)
29+
cleanup_fixture_git() {
30+
if [ "$CREATED_FIXTURE_GIT" = true ] && [ -d "$FIXTURE_DIR/.git" ]; then
31+
rm -rf "$FIXTURE_DIR/.git"
32+
fi
33+
}
34+
trap cleanup_fixture_git EXIT
35+
36+
# Create a requirements file pointing to the local fixture
37+
REQUIREMENTS_FILE="$OUTDIR/test-requirements.txt"
38+
echo "test_build_failure @ ${FIXTURE_URL}" > "$REQUIREMENTS_FILE"
39+
40+
# Run bootstrap in test mode
41+
# - Package resolves from local git repo
42+
# - Build fails (broken build backend)
43+
# - Prebuilt fallback fails (package not on PyPI)
44+
# - Failure should be recorded
45+
set +e
46+
fromager \
47+
--log-file="$OUTDIR/bootstrap.log" \
48+
--error-log-file="$OUTDIR/fromager-errors.log" \
49+
--sdists-repo="$OUTDIR/sdists-repo" \
50+
--wheels-repo="$OUTDIR/wheels-repo" \
51+
--work-dir="$OUTDIR/work-dir" \
52+
bootstrap --test-mode -r "$REQUIREMENTS_FILE"
53+
EXIT_CODE=$?
54+
set -e
55+
56+
pass=true
57+
58+
# Check 1: Exit code should be 1 (failures recorded)
59+
if [ "$EXIT_CODE" -ne 1 ]; then
60+
echo "FAIL: Expected exit code 1, got $EXIT_CODE" 1>&2
61+
pass=false
62+
fi
63+
64+
# Check 2: The test-mode-failures JSON file should exist
65+
FAILURES_FILE=$(find "$OUTDIR/work-dir" -name "test-mode-failures-*.json" 2>/dev/null | head -1)
66+
if [ -z "$FAILURES_FILE" ] || [ ! -f "$FAILURES_FILE" ]; then
67+
echo "FAIL: test-mode-failures-*.json file not found" 1>&2
68+
pass=false
69+
else
70+
echo "Found failures file: $FAILURES_FILE"
71+
72+
# Check 3: test_build_failure should be in failed packages
73+
# Note: package name uses underscore as recorded by fromager
74+
if ! jq -e '.failures[] | select(.package == "test_build_failure")' "$FAILURES_FILE" > /dev/null 2>&1; then
75+
echo "FAIL: Expected 'test_build_failure' in failed packages" 1>&2
76+
jq '.' "$FAILURES_FILE" 1>&2
77+
pass=false
78+
fi
79+
80+
# Check 4: failure_type MUST be 'bootstrap' (actual build failure, not resolution)
81+
# Pinning to 'bootstrap' catches regressions if fromager misclassifies failures
82+
FAILURE_TYPE=$(jq -r '[.failures[] | select(.package == "test_build_failure")][0].failure_type' "$FAILURES_FILE")
83+
if [ "$FAILURE_TYPE" != "bootstrap" ]; then
84+
echo "FAIL: Expected failure_type 'bootstrap', got '$FAILURE_TYPE'" 1>&2
85+
pass=false
86+
else
87+
echo "Failure type: $FAILURE_TYPE"
88+
fi
89+
90+
# Check 5: exception_message should indicate a build-related error
91+
EXCEPTION_MSG=$(jq -r '[.failures[] | select(.package == "test_build_failure")][0].exception_message' "$FAILURES_FILE")
92+
if [[ "$EXCEPTION_MSG" != *"nonexistent_file"* ]] && [[ "$EXCEPTION_MSG" != *"MANIFEST"* ]] && [[ "$EXCEPTION_MSG" != *"build"* ]] && [[ "$EXCEPTION_MSG" != *"CalledProcessError"* ]]; then
93+
echo "FAIL: Expected exception message about build failure, got: $EXCEPTION_MSG" 1>&2
94+
pass=false
95+
fi
96+
fi
97+
98+
# Check 6: Log should show test mode enabled
99+
if ! grep -q "test mode enabled" "$OUTDIR/bootstrap.log"; then
100+
echo "FAIL: Log should contain 'test mode enabled'" 1>&2
101+
pass=false
102+
fi
103+
104+
# Check 7: Log may show fallback attempt (depends on where failure occurs)
105+
# Note: Fallback is only attempted when build fails after resolution succeeds.
106+
# Our fixture fails during metadata extraction, so fallback may not be triggered.
107+
if grep -q "pre-built fallback" "$OUTDIR/bootstrap.log"; then
108+
echo "INFO: Fallback was attempted (package not on PyPI, so it failed)"
109+
else
110+
echo "INFO: No fallback attempt (failure occurred before build phase)"
111+
fi
112+
113+
$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: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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: Patch application must be attempted
80+
if ! grep -q "applying patch file.*break-build.patch" "$OUTDIR/bootstrap.log"; then
81+
echo "FAIL: Expected patch 'break-build.patch' to be applied" 1>&2
82+
pass=false
83+
else
84+
echo "Patch application was attempted"
85+
fi
86+
87+
# Check 4: Prebuilt fallback MUST be triggered and succeed
88+
if ! grep -q "pre-built fallback" "$OUTDIR/bootstrap.log"; then
89+
echo "FAIL: Expected prebuilt fallback to be triggered" 1>&2
90+
pass=false
91+
elif ! grep -q "successfully used pre-built wheel" "$OUTDIR/bootstrap.log"; then
92+
echo "FAIL: Prebuilt fallback was triggered but did not succeed" 1>&2
93+
pass=false
94+
else
95+
echo "SUCCESS: Prebuilt fallback triggered and succeeded"
96+
fi
97+
98+
# Check 5: No failures should be recorded (fallback succeeded)
99+
FAILURES_FILE=$(find "$OUTDIR/work-dir" -name "test-mode-failures-*.json" 2>/dev/null | head -1)
100+
if [ -n "$FAILURES_FILE" ] && [ -f "$FAILURES_FILE" ]; then
101+
FAILURE_COUNT=$(jq '.failures | length' "$FAILURES_FILE")
102+
if [ "$FAILURE_COUNT" -gt 0 ]; then
103+
echo "FAIL: Expected no failures (fallback should succeed), got $FAILURE_COUNT" 1>&2
104+
jq '.failures[] | {package, failure_type, exception_type}' "$FAILURES_FILE" 1>&2
105+
pass=false
106+
fi
107+
fi
108+
109+
# Check 6: Verify test mode completed
110+
if grep -q "test mode:" "$OUTDIR/bootstrap.log"; then
111+
echo "Test mode processing completed"
112+
else
113+
echo "NOTE: Test mode summary not found in log" 1>&2
114+
fi
115+
116+
$pass

0 commit comments

Comments
 (0)