Skip to content

Commit 597eb89

Browse files
committed
chore(common): add unit tests for release
1 parent 99415bb commit 597eb89

3 files changed

Lines changed: 208 additions & 1 deletion

File tree

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ ROOT_DIR := $(CURDIR)
66
BUILDPACK_DIR := $(ROOT_DIR)
77
PYTHON_BIN ?= python3
88

9-
.PHONY: test-buildpack clean-test-buildpack test-detect test-compile smoke-test start-local
9+
.PHONY: test-buildpack clean-test-buildpack test-detect test-compile test-release smoke-test start-local
1010

1111
# Reset the temporary staging directories used for local buildpack testing.
1212
clean-test-buildpack:
@@ -20,6 +20,10 @@ test-detect:
2020
test-compile:
2121
@bash ./test/unit/compile_test.sh
2222

23+
# Run unit tests for the release script command-resolution logic.
24+
test-release:
25+
@bash ./test/unit/release_test.sh
26+
2327
# Run the sample app through detect, compile, and release using the same
2428
# temporary directories each time so local testing is repeatable.
2529
test-buildpack: clean-test-buildpack

bin/release

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ if [ -z "$DEFAULT_WEB_PROCESS" ] && [ -f "app.py" ]; then
6262
DEFAULT_WEB_PROCESS="${PYTHON_BIN} app.py"
6363
fi
6464

65+
if [ -z "$DEFAULT_WEB_PROCESS" ]; then
66+
DEFAULT_WEB_PROCESS="${PYTHON_BIN} -m uvicorn main:app --host 0.0.0.0 --port \${PORT:-8000}"
67+
fi
68+
6569
cat <<EOF
6670
---
6771
default_process_types:

test/unit/release_test.sh

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
6+
RELEASE_SCRIPT="$ROOT_DIR/bin/release"
7+
TMP_DIR="$(mktemp -d /tmp/release-test.XXXXXX)"
8+
TEST_ROOT="$TMP_DIR/test-root"
9+
FAKE_BIN_DIR="$TMP_DIR/fake-bin"
10+
REAL_PYTHON3="$(command -v python3 || true)"
11+
12+
cleanup() {
13+
rm -rf "$TMP_DIR"
14+
}
15+
16+
trap cleanup EXIT
17+
18+
assert_exit_code() {
19+
local actual="$1"
20+
local expected="$2"
21+
local message="$3"
22+
23+
if [ "$actual" -ne "$expected" ]; then
24+
echo "FAIL: $message"
25+
echo "Expected exit code: $expected"
26+
echo "Actual exit code: $actual"
27+
exit 1
28+
fi
29+
}
30+
31+
assert_contains() {
32+
local actual="$1"
33+
local expected="$2"
34+
local message="$3"
35+
36+
if [[ "$actual" != *"$expected"* ]]; then
37+
echo "FAIL: $message"
38+
echo "Expected to find: $expected"
39+
echo "Actual output: $actual"
40+
exit 1
41+
fi
42+
}
43+
44+
assert_not_contains() {
45+
local actual="$1"
46+
local unexpected="$2"
47+
local message="$3"
48+
49+
if [[ "$actual" == *"$unexpected"* ]]; then
50+
echo "FAIL: $message"
51+
echo "Did not expect to find: $unexpected"
52+
echo "Actual output: $actual"
53+
exit 1
54+
fi
55+
}
56+
57+
setup_fake_python_commands() {
58+
if [ -z "$REAL_PYTHON3" ]; then
59+
echo "FAIL: python3 is required to run release unit tests"
60+
exit 1
61+
fi
62+
63+
mkdir -p "$FAKE_BIN_DIR"
64+
65+
cat > "$FAKE_BIN_DIR/python3" <<'EOF'
66+
#!/usr/bin/env bash
67+
set -euo pipefail
68+
exec "$REAL_PYTHON3" "$@"
69+
EOF
70+
71+
chmod +x "$FAKE_BIN_DIR/python3"
72+
}
73+
74+
run_release() {
75+
local app_dir="$1"
76+
local output_file="$TEST_ROOT/output.txt"
77+
78+
set +e
79+
(
80+
cd "$app_dir"
81+
PATH="$FAKE_BIN_DIR:/usr/bin:/bin" REAL_PYTHON3="$REAL_PYTHON3" "$RELEASE_SCRIPT"
82+
) >"$output_file" 2>&1
83+
status=$?
84+
set -e
85+
86+
output="$(cat "$output_file")"
87+
}
88+
89+
test_release_exits_when_procfile_exists() {
90+
# Arrange
91+
local app_dir="$TEST_ROOT/procfile-app"
92+
mkdir -p "$app_dir"
93+
touch "$app_dir/Procfile"
94+
setup_fake_python_commands
95+
96+
# Act
97+
run_release "$app_dir"
98+
99+
# Assert
100+
assert_exit_code "$status" 0 "release should exit successfully when a Procfile exists"
101+
assert_contains "$output" "" "release should not emit default process types when Procfile exists"
102+
}
103+
104+
test_release_prefers_project_name_script_over_start() {
105+
# Arrange
106+
local app_dir="$TEST_ROOT/project-script-app"
107+
mkdir -p "$app_dir"
108+
setup_fake_python_commands
109+
cat > "$app_dir/pyproject.toml" <<'EOF'
110+
[project]
111+
name = "my-app"
112+
113+
[project.scripts]
114+
my-app = "server.main:run"
115+
start = "server.main:start"
116+
EOF
117+
118+
# Act
119+
run_release "$app_dir"
120+
121+
# Assert
122+
assert_exit_code "$status" 0 "release should succeed for pyproject-based apps"
123+
assert_contains "$output" 'web: python3 -c "from server.main import run; run()"' "release should prefer the script named after project.name"
124+
assert_not_contains "$output" 'start; start()' "release should not choose the start script when a project-name script exists"
125+
}
126+
127+
test_release_falls_back_to_start_script() {
128+
# Arrange
129+
local app_dir="$TEST_ROOT/start-script-app"
130+
mkdir -p "$app_dir"
131+
setup_fake_python_commands
132+
cat > "$app_dir/pyproject.toml" <<'EOF'
133+
[project]
134+
name = "my-app"
135+
136+
[project.scripts]
137+
start = "server.main:start"
138+
EOF
139+
140+
# Act
141+
run_release "$app_dir"
142+
143+
# Assert
144+
assert_exit_code "$status" 0 "release should succeed when only a start script is defined"
145+
assert_contains "$output" 'web: python3 -c "from server.main import start; start()"' "release should fall back to the start script"
146+
}
147+
148+
test_release_falls_back_to_main_py() {
149+
# Arrange
150+
local app_dir="$TEST_ROOT/main-py-app"
151+
mkdir -p "$app_dir"
152+
touch "$app_dir/main.py"
153+
setup_fake_python_commands
154+
155+
# Act
156+
run_release "$app_dir"
157+
158+
# Assert
159+
assert_exit_code "$status" 0 "release should succeed for apps with main.py"
160+
assert_contains "$output" 'web: python3 main.py' "release should use main.py when no scripts are configured"
161+
}
162+
163+
test_release_falls_back_to_app_py() {
164+
# Arrange
165+
local app_dir="$TEST_ROOT/app-py-app"
166+
mkdir -p "$app_dir"
167+
touch "$app_dir/app.py"
168+
setup_fake_python_commands
169+
170+
# Act
171+
run_release "$app_dir"
172+
173+
# Assert
174+
assert_exit_code "$status" 0 "release should succeed for apps with app.py"
175+
assert_contains "$output" 'web: python3 app.py' "release should use app.py when main.py is absent"
176+
}
177+
178+
test_release_uses_uvicorn_fallback_when_no_entrypoint_exists() {
179+
# Arrange
180+
local app_dir="$TEST_ROOT/uvicorn-fallback-app"
181+
mkdir -p "$app_dir"
182+
setup_fake_python_commands
183+
184+
# Act
185+
run_release "$app_dir"
186+
187+
# Assert
188+
assert_exit_code "$status" 0 "release should still succeed when no explicit entrypoint exists"
189+
assert_contains "$output" 'web: python3 -m uvicorn main:app --host 0.0.0.0 --port ${PORT:-8000}' "release should emit the uvicorn fallback command"
190+
}
191+
192+
test_release_exits_when_procfile_exists
193+
test_release_prefers_project_name_script_over_start
194+
test_release_falls_back_to_start_script
195+
test_release_falls_back_to_main_py
196+
test_release_falls_back_to_app_py
197+
test_release_uses_uvicorn_fallback_when_no_entrypoint_exists
198+
199+
echo "PASS: release unit tests"

0 commit comments

Comments
 (0)