Skip to content

Commit 29ce1ec

Browse files
Lasse Benningaclaude
andcommitted
fix(grading): stop autograder from silently crashing under bash 5
`((score += l1))`-style compound assignment is a false command in bash whenever the resulting value is 0 -- exit status 1 even though the assignment succeeded. Under `set -e` (used by every autograder here), that silently kills the script mid-run on GitHub Actions' Ubuntu runners (bash 5), while working fine locally on macOS (bash 3.2, which does not enforce this). Any student whose running score hits exactly 0 at a checkpoint -- most commonly an early, mostly-empty submission -- got a grading run that failed with a bare "exit code 1" and no score.json update, not a real 0 score. Reproduced locally with bash 5 against an empty scaffold; confirmed exit 0 after the fix, and re-verified real solution runs still score correctly. Fix: rewrite every `((var += expr))`/`((var -= expr))` as `var=$((var + expr))`, and guard bare check_*() calls that can legitimately return non-zero (a warn/fail signal, not a script error) with `|| true` so `set -e` does not treat them as fatal. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 79426e8 commit 29ce1ec

1 file changed

Lines changed: 32 additions & 32 deletions

File tree

.hyf/test.sh

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,24 @@ warn() { _grader_details+=("WARN: $1"); }
2020
l1=0
2121
for f in Dockerfile "src/pipeline.py" "tests/test_pipeline.py" "AI_ASSIST.md"; do
2222
if [[ -f "$REPO_ROOT/$f" ]]; then
23-
((l1 += 3))
23+
l1=$((l1 + 3))
2424
else
2525
fail "missing $f"
2626
fi
2727
done
2828
# ci.yml
2929
if ls "$REPO_ROOT/.github/workflows/"*.yml 2>/dev/null | grep -q .; then
30-
((l1 += 2))
30+
l1=$((l1 + 2))
3131
else
3232
fail "missing .github/workflows/*.yml"
3333
fi
3434
# requirements.txt or pyproject.toml
3535
if [[ -f "$REPO_ROOT/requirements.txt" ]] || [[ -f "$REPO_ROOT/pyproject.toml" ]]; then
36-
((l1 += 1))
36+
l1=$((l1 + 1))
3737
else
3838
fail "missing requirements.txt or pyproject.toml"
3939
fi
40-
((score += l1))
40+
score=$((score + l1))
4141
pass "Level 1: required files ($l1/15 pts)"
4242

4343

@@ -46,7 +46,7 @@ l2=0
4646
df="$REPO_ROOT/Dockerfile"
4747
if [[ -f "$df" ]]; then
4848
if grep -qE "^FROM\s+python:3\.11-slim" "$df"; then
49-
((l2 += 5)); pass "Dockerfile uses python:3.11-slim base image"
49+
l2=$((l2 + 5)); pass "Dockerfile uses python:3.11-slim base image"
5050
else
5151
fail "Dockerfile does not use python:3.11-slim base image"
5252
fi
@@ -55,18 +55,18 @@ if [[ -f "$df" ]]; then
5555
req_line=$(grep -n "COPY.*requirements" "$df" | head -1 | cut -d: -f1 || echo 0)
5656
src_line=$(grep -n "COPY.*src" "$df" | head -1 | cut -d: -f1 || echo 9999)
5757
if [[ "$req_line" -gt 0 && "$src_line" -lt 9999 && "$req_line" -lt "$src_line" ]]; then
58-
((l2 += 7)); pass "Dockerfile copies requirements before source (cache-friendly)"
58+
l2=$((l2 + 7)); pass "Dockerfile copies requirements before source (cache-friendly)"
5959
else
6060
fail "Dockerfile does not copy requirements before source code"
6161
fi
6262

6363
if grep -qE "^CMD" "$df"; then
64-
((l2 += 3)); pass "Dockerfile has a CMD instruction"
64+
l2=$((l2 + 3)); pass "Dockerfile has a CMD instruction"
6565
else
6666
fail "Dockerfile missing CMD instruction"
6767
fi
6868
fi
69-
((score += l2))
69+
score=$((score + l2))
7070
pass "Level 2: Dockerfile ($l2/15 pts)"
7171

7272
# ── Level 3 (10 pts): unit tests ─────────────────────────────────────────────
@@ -75,67 +75,67 @@ test_file="$REPO_ROOT/tests/test_pipeline.py"
7575
if [[ -f "$test_file" ]]; then
7676
test_count=$(grep -cE "^[[:space:]]*def test_" "$test_file" || true)
7777
if [[ "$test_count" -ge 2 ]]; then
78-
((l3 += 7)); pass "tests/test_pipeline.py has $test_count test functions (≥2 required)"
78+
l3=$((l3 + 7)); pass "tests/test_pipeline.py has $test_count test functions (≥2 required)"
7979
else
8080
fail "tests/test_pipeline.py has only $test_count test function(s) — at least 2 required"
8181
fi
8282
if ! grep -q "NotImplementedError" "$test_file"; then
83-
((l3 += 3)); pass "tests/test_pipeline.py has no NotImplementedError stubs remaining"
83+
l3=$((l3 + 3)); pass "tests/test_pipeline.py has no NotImplementedError stubs remaining"
8484
else
8585
fail "tests/test_pipeline.py still contains NotImplementedError stubs"
8686
fi
8787
fi
88-
((score += l3))
88+
score=$((score + l3))
8989
pass "Level 3: unit tests ($l3/10 pts)"
9090

9191
# ── Level 4 (10 pts): pinned dependencies ───────────────────────────────────
9292
l4=0
9393
if [[ -f "$REPO_ROOT/requirements.txt" ]]; then
9494
pinned=$(grep -cE "^[a-zA-Z].*==" "$REPO_ROOT/requirements.txt" || true)
9595
if [[ "$pinned" -ge 1 ]]; then
96-
((l4 += 7)); pass "requirements.txt has $pinned pinned package(s)"
96+
l4=$((l4 + 7)); pass "requirements.txt has $pinned pinned package(s)"
9797
else
9898
fail "requirements.txt has no pinned packages (use package==version)"
9999
fi
100100
fi
101101
if [[ -f "$REPO_ROOT/uv.lock" ]]; then
102-
((l4 += 3)); pass "uv.lock present (full dependency tree pinned)"
102+
l4=$((l4 + 3)); pass "uv.lock present (full dependency tree pinned)"
103103
elif [[ "$l4" -ge 7 ]]; then
104-
((l4 += 3)); pass "requirements.txt pins satisfied (no uv.lock needed)"
104+
l4=$((l4 + 3)); pass "requirements.txt pins satisfied (no uv.lock needed)"
105105
fi
106-
((score += l4))
106+
score=$((score + l4))
107107
pass "Level 4: pinned dependencies ($l4/10 pts)"
108108

109109
# ── Level 5 (20 pts): CI workflow ────────────────────────────────────────────
110110
l5=0
111111
ci_file=$(ls "$REPO_ROOT/.github/workflows/"*.yml 2>/dev/null | head -1 || true)
112112
if [[ -n "$ci_file" ]]; then
113-
grep -q "pull_request" "$ci_file" && { ((l5 += 4)); pass "ci.yml triggers on pull_request"; } || fail "ci.yml missing pull_request trigger"
114-
grep -qE '\bmain\b' "$ci_file" && { ((l5 += 4)); pass "ci.yml triggers on push to main"; } || fail "ci.yml missing push to main trigger"
115-
grep -q "ruff check" "$ci_file" && { ((l5 += 3)); pass "ci.yml runs ruff check (lint)"; } || fail "ci.yml missing ruff check step"
116-
grep -q "ruff format" "$ci_file" && { ((l5 += 3)); pass "ci.yml runs ruff format (format check)"; } || fail "ci.yml missing ruff format step"
117-
grep -q "pytest" "$ci_file" && { ((l5 += 3)); pass "ci.yml runs pytest"; } || fail "ci.yml missing pytest step"
118-
grep -q "docker build" "$ci_file" && { ((l5 += 3)); pass "ci.yml runs docker build"; } || fail "ci.yml missing docker build step"
113+
grep -q "pull_request" "$ci_file" && { l5=$((l5 + 4)); pass "ci.yml triggers on pull_request"; } || fail "ci.yml missing pull_request trigger"
114+
grep -qE '\bmain\b' "$ci_file" && { l5=$((l5 + 4)); pass "ci.yml triggers on push to main"; } || fail "ci.yml missing push to main trigger"
115+
grep -q "ruff check" "$ci_file" && { l5=$((l5 + 3)); pass "ci.yml runs ruff check (lint)"; } || fail "ci.yml missing ruff check step"
116+
grep -q "ruff format" "$ci_file" && { l5=$((l5 + 3)); pass "ci.yml runs ruff format (format check)"; } || fail "ci.yml missing ruff format step"
117+
grep -q "pytest" "$ci_file" && { l5=$((l5 + 3)); pass "ci.yml runs pytest"; } || fail "ci.yml missing pytest step"
118+
grep -q "docker build" "$ci_file" && { l5=$((l5 + 3)); pass "ci.yml runs docker build"; } || fail "ci.yml missing docker build step"
119119
fi
120-
((score += l5))
120+
score=$((score + l5))
121121
pass "Level 5: CI workflow ($l5/20 pts)"
122122

123123
# ── Level 6 (15 pts): env-var configuration ──────────────────────────────────
124124
l6=0
125125
py="$REPO_ROOT/src/pipeline.py"
126126
if [[ -f "$py" ]]; then
127127
if grep -qE "os\.(environ|getenv)|from os import (environ|getenv)" "$py"; then
128-
((l6 += 10)); pass "pipeline.py reads config from os.environ/os.getenv"
128+
l6=$((l6 + 10)); pass "pipeline.py reads config from os.environ/os.getenv"
129129
else
130130
fail "pipeline.py does not read from os.environ or os.getenv"
131131
fi
132132
if ! grep -q "NotImplementedError" "$py"; then
133-
((l6 += 5)); pass "pipeline.py has no NotImplementedError stubs remaining"
133+
l6=$((l6 + 5)); pass "pipeline.py has no NotImplementedError stubs remaining"
134134
else
135135
fail "pipeline.py still contains NotImplementedError"
136136
fi
137137
fi
138-
((score += l6))
138+
score=$((score + l6))
139139
pass "Level 6: env-var config ($l6/15 pts)"
140140

141141
# ── Level 7 (10 pts): ACR screenshot ────────────────────────────────────────
@@ -144,14 +144,14 @@ screenshot="$REPO_ROOT/assets/acr_push_week5.png"
144144
if [[ -f "$screenshot" ]]; then
145145
size=$(wc -c < "$screenshot")
146146
if [[ "$size" -gt 1024 ]]; then
147-
((l7 += 10)); pass "assets/acr_push_week5.png present and non-trivial (${size} bytes)"
147+
l7=$((l7 + 10)); pass "assets/acr_push_week5.png present and non-trivial (${size} bytes)"
148148
else
149149
fail "assets/acr_push_week5.png exists but looks empty (${size} bytes)"
150150
fi
151151
else
152152
fail "assets/acr_push_week5.png missing (Task 7 deliverable)"
153153
fi
154-
((score += l7))
154+
score=$((score + l7))
155155
pass "Level 7: ACR screenshot ($l7/10 pts)"
156156

157157
# ── Level 8 (5 pts): AI_ASSIST.md content ───────────────────────────────────
@@ -165,22 +165,22 @@ if [[ -f "$ai" ]]; then
165165
has_todo=$(grep -c "^TODO:" "$ai" || true)
166166

167167
if [[ "$has_prompt" -ge 1 && "$has_code" -ge 1 && "$has_changed" -ge 1 ]]; then
168-
((l8 += 3)); pass "AI_ASSIST.md has all three required sections"
168+
l8=$((l8 + 3)); pass "AI_ASSIST.md has all three required sections"
169169
else
170170
fail "AI_ASSIST.md missing one or more required sections"
171171
fi
172172
if [[ "$chars" -gt 500 && "$has_todo" -eq 0 ]]; then
173-
((l8 += 2)); pass "AI_ASSIST.md is filled in (${chars} chars, no TODO placeholders)"
173+
l8=$((l8 + 2)); pass "AI_ASSIST.md is filled in (${chars} chars, no TODO placeholders)"
174174
else
175175
fail "AI_ASSIST.md still contains TODO placeholders or is too short (${chars} chars)"
176176
fi
177177
fi
178-
((score += l8))
178+
score=$((score + l8))
179179
pass "Level 8: AI report ($l8/5 pts)"
180180

181181
# ── Code hygiene (warnings from grader_lib) ──────────────────────────────────
182-
check_no_print_statements "$REPO_ROOT/src" "src/"
183-
check_gitignore_python "$REPO_ROOT/.gitignore"
182+
check_no_print_statements "$REPO_ROOT/src" "src/" || true
183+
check_gitignore_python "$REPO_ROOT/.gitignore" || true
184184

185185
# ── Final result ─────────────────────────────────────────────────────────────
186186
passing_score=60

0 commit comments

Comments
 (0)