33# Source this at the top of test.sh:
44# source "$(dirname "$0")/grader_lib.sh"
55#
6- # Provides: pass(), fail(), warn(), print_results(), write_score(),
7- # and a set of common static-analysis checks derived from recurring
8- # PR review patterns across cohort c55 .
6+ # Design rule: every check_* function always returns 0.
7+ # Results are communicated via pass()/fail()/warn() into _grader_details,
8+ # never via exit code. This makes bare calls safe under `set -euo pipefail` .
99
1010_grader_details=()
1111
3939}
4040
4141# ── Common static-analysis checks ────────────────────────────────────────────
42- # Each function: returns 0 on pass, 1 on fail/warn (for caller logic).
43- # All feedback goes through pass()/fail()/warn() so it appears in print_results.
42+ # All check_* functions always return 0. Findings are recorded via
43+ # pass()/fail()/warn() and printed by print_results() .
4444
4545check_no_print_statements () {
4646 # Usage: check_no_print_statements <dir> [label]
@@ -53,7 +53,6 @@ check_no_print_statements() {
5353 local count
5454 count=$( echo " $found " | wc -l | tr -d ' ' )
5555 warn " $label : $count print() call(s) found — use logging.info/warning/error instead (see Week 1 Ch1)"
56- return 1
5756 fi
5857 return 0
5958}
@@ -67,7 +66,6 @@ check_no_notimplemented() {
6766 found=$( grep -rn " raise NotImplementedError" " $dir " --include=" *.py" 2> /dev/null || true)
6867 if [[ -n " $found " ]]; then
6968 fail " $label : raise NotImplementedError still present — remove stubs before submitting"
70- return 1
7169 fi
7270 return 0
7371}
@@ -83,7 +81,6 @@ check_no_relative_imports() {
8381 found=$( grep -rn " ^from \." " $dir " --include=" *.py" 2> /dev/null || true)
8482 if [[ -n " $found " ]]; then
8583 fail " $label : relative import found (from .module) — use absolute: 'from src.module import x'"
86- return 1
8784 fi
8885 return 0
8986}
@@ -95,7 +92,6 @@ check_no_logging_in_utils() {
9592 if [[ ! -f " $file " ]]; then return 0; fi
9693 if grep -qE " logging\.basicConfig|logging\.getLogger" " $file " ; then
9794 warn " $file : logging.basicConfig/getLogger found — logging setup belongs in cleaner.py or the entry-point, not in utils"
98- return 1
9995 fi
10096 return 0
10197}
@@ -106,7 +102,7 @@ check_gitignore_python() {
106102 local gi=" ${1:- .gitignore} "
107103 if [[ ! -f " $gi " ]]; then
108104 warn " .gitignore is missing — add one so __pycache__/ and *.pyc are not committed"
109- return 1
105+ return 0
110106 fi
111107 local ok=true
112108 if ! grep -q " __pycache__" " $gi " ; then
@@ -122,36 +118,34 @@ check_gitignore_python() {
122118 ok=false
123119 fi
124120 if [[ " $ok " = true ]]; then pass " .gitignore correctly excludes __pycache__/, *.pyc, and .env" ; fi
121+ return 0
125122}
126123
127124check_screenshot_is_png () {
128- # Usage: check_screenshot_is_png <expected_path> [<wrong_ext_glob>]
129- # Awards full credit for .png, warns (and still credits) for .jpg/.jpeg,
130- # zero for missing. Matches the pattern flagged in c55 PR reviews.
125+ # Usage: check_screenshot_is_png <expected_path>
126+ # Awards full credit for .png, warns for .jpg/.jpeg, fails if missing.
131127 local expected_png=" $1 "
132128 local dir
133129 dir=" $( dirname " $expected_png " ) "
134130 local base
135131 base=" $( basename " $expected_png " .png) "
136-
137132 if [[ -s " $expected_png " ]]; then
138133 pass " screenshot is $expected_png (.png format ✓)"
139134 return 0
140135 fi
141136 for ext in jpg jpeg; do
142137 if [[ -s " $dir /$base .$ext " ]]; then
143138 warn " screenshot is .$ext but should be .png — rename to $base .png (partial credit still given)"
144- return 1
139+ return 0
145140 fi
146141 done
147142 fail " screenshot missing: $expected_png not found"
148- return 2
143+ return 0
149144}
150145
151146check_silent_zero_in_except () {
152147 # Usage: check_silent_zero_in_except <file>
153- # Detects the pattern: try: x = compute() / except: x = 0
154- # which silently corrupts data instead of skipping or raising.
148+ # Detects: try: x = compute() / except: x = 0 (silent data corruption).
155149 local file=" $1 "
156150 if [[ ! -f " $file " ]]; then return 0; fi
157151 local found
@@ -170,16 +164,14 @@ for node in ast.walk(tree):
170164PY
171165)
172166 if [[ -n " $found " ]]; then
173- warn " $file : silent 0-assignment in except block — skip the row or raise instead of setting to 0:\n $found "
174- return 1
167+ warn " $file : silent 0-assignment in except block — skip the row or raise instead:\n $found "
175168 fi
176169 return 0
177170}
178171
179172check_exception_logged () {
180173 # Usage: check_exception_logged <dir>
181- # Warns when except blocks log/print a message but don't include the
182- # exception variable (e, err, exc), meaning the error type is lost.
174+ # Warns when an except block logs a message but omits the exception variable.
183175 local dir=" ${1:- .} "
184176 local found
185177 found=$( python3 - " $dir " 2> /dev/null << 'PY '
@@ -197,54 +189,50 @@ for root, _, files in os.walk(sys.argv[1]):
197189 for node in ast.walk(tree):
198190 if not isinstance(node, ast.ExceptHandler):
199191 continue
200- exc_var = node.name # e.g. "e" in `except ValueError as e`
192+ exc_var = node.name
201193 if not exc_var:
202194 continue
203195 for stmt in node.body:
204196 for call in ast.walk(stmt):
205197 if not isinstance(call, ast.Call):
206198 continue
207- # Is it a logging.* or print call?
208199 func = call.func
209200 is_log = (isinstance(func, ast.Attribute) and
210201 isinstance(func.value, ast.Name) and
211202 func.value.id == "logging")
212203 is_print = isinstance(func, ast.Name) and func.id == "print"
213204 if not (is_log or is_print):
214205 continue
215- # Does the call reference the exception variable?
216206 src = ast.unparse(call)
217207 if exc_var not in src:
218- issues.append(f"{path}:{call.lineno}: log message doesn't include exception variable '{exc_var}' — add it for easier debugging")
208+ issues.append(f"{path}:{call.lineno}: log message doesn't include '{exc_var}' — add it for easier debugging")
219209if issues:
220- for i in issues[:3]: # cap at 3 to keep output readable
210+ for i in issues[:3]:
221211 print(i)
222212PY
223213)
224214 if [[ -n " $found " ]]; then
225- warn " exception variable not included in log message (harder to debug):\n $found "
226- return 1
215+ warn " exception variable missing from log message:\n $found "
227216 fi
228217 return 0
229218}
230219
231220check_ruff () {
232221 # Usage: check_ruff <dir> [<select>]
233- # Runs ruff on <dir> if available; warns on violations.
234- # Default select: F401 (unused imports), E302 (missing blank lines).
222+ # Runs ruff if available; warns on F401 (unused imports) and E302/E303 (blank lines).
235223 local dir=" ${1:- .} "
236224 local select=" ${2:- F401,E302,E303} "
237225 if ! command -v ruff & > /dev/null && ! python3 -m ruff --version & > /dev/null 2>&1 ; then
238- return 0 # ruff not installed — skip silently
226+ return 0
239227 fi
240228 local out
241229 out=$( python3 -m ruff check --select=" $select " --output-format=text " $dir " 2> /dev/null || true)
242230 if [[ -n " $out " ]]; then
243231 local count
244232 count=$( echo " $out " | grep -c " \.py:" || true)
245233 warn " $dir : ruff found $count style issue(s) (unused imports / missing blank lines) — run 'ruff check $dir ' to see details"
246- return 1
234+ else
235+ pass " $dir : no ruff style issues (F401/E302/E303)"
247236 fi
248- pass " $dir : no ruff style issues (F401/E302/E303)"
249237 return 0
250238}
0 commit comments