@@ -47,7 +47,11 @@ tmp_dir=$(mktemp -d -t "factor-runner-${slug}-XXXXX")
4747trap ' rm -rf "${tmp_dir}"' EXIT
4848cp -r " ${solution_dir} /." " ${tmp_dir} "
4949stripped_tests=" ${tmp_dir} /${slug} /${slug} -tests.factor"
50- awk ' !/^STOP-HERE$/' " ${stripped_tests} " > " ${stripped_tests} .new"
50+ # Blank each STOP-HERE line rather than deleting it: a blank line is a no-op
51+ # to Factor, and keeping the line count intact means the line numbers Factor
52+ # reports for failures still match the file the student submitted.
53+ awk ' { if ($0 ~ /^STOP-HERE$/) print ""; else print }' \
54+ " ${stripped_tests} " > " ${stripped_tests} .new"
5155mv " ${stripped_tests} .new" " ${stripped_tests} "
5256
5357# Run Factor; capture combined stdout/stderr.
@@ -131,7 +135,7 @@ src_tests=$(awk "${awk_json}"'
131135
132136# 2. Parse Factor stdout into NDJSON segments and failures:
133137# segments: {"type":"segment","idx":N,"failed":bool,"name":"...","output":"..."}
134- # failures: {"type":"failure","line_no":N,"message":"..."}
138+ # failures: {"type":"failure","line_no":N,"location":"..."," message":"..."}
135139parsed=$( printf ' %s\n' " ${raw_output} " | awk " ${awk_json} " '
136140 function close_segment( out, i) {
137141 if (idx == 0) return
@@ -147,8 +151,8 @@ parsed=$(printf '%s\n' "${raw_output}" | awk "${awk_json}"'
147151 for (i = 1; i <= fail_n; i++) body = body (i > 1 ? "\n" : "") fail[i]
148152 sub(/^\n+/, "", body)
149153 sub(/\n+$/, "", body)
150- printf "{\"type\":\"failure\",\"line_no\":%d,\"message\":%s}\n",
151- fail_line, json_str(body)
154+ printf "{\"type\":\"failure\",\"line_no\":%d,\"location\":%s,\" message\":%s}\n",
155+ fail_line, json_str(fail_loc), json_str( body)
152156 }
153157 # Factor renders each test-word name into a title, e.g.:
154158 # unit-test → "Unit Test:"
@@ -221,8 +225,14 @@ parsed=$(printf '%s\n' "${raw_output}" | awk "${awk_json}"'
221225 n = substr($0, RSTART, RLENGTH)
222226 gsub(/[^0-9]/, "", n)
223227 fail_line = n + 0
228+ # exercism-tools prints "<path>: <line#>"; collapse it to the
229+ # conventional "<path>:<line#>" so the student can paste it
230+ # into an editor. The path is already relative to the solution
231+ # root, so it is safe to show as-is.
232+ fail_loc = substr($0, 1, RSTART - 1) ":" fail_line
224233 } else {
225234 fail_line = 0
235+ fail_loc = ""
226236 }
227237 fail_n = 0
228238 delete fail
@@ -276,15 +286,16 @@ jq -n \
276286 --argjson segs " $( printf ' %s\n' " ${segments} " | jq -s ' .' ) " \
277287 --argjson fails " $( printf ' %s\n' " ${failures} " | jq -s ' .' ) " \
278288 '
279- ($fails | map({(.line_no|tostring): .message }) | add // {}) as $fail_by_line
289+ ($fails | map({(.line_no|tostring): .}) | add // {}) as $fail_by_line
280290 | $segs | sort_by(.idx)
281291 | to_entries
282292 | map(
283293 .value as $seg
284294 | (.key) as $i
285295 | ($srcs[$i] // null) as $src
286296 | ($src.line_no | tostring) as $ln
287- | ($fail_by_line[$ln] // null) as $msg
297+ | ($fail_by_line[$ln] // null) as $fail
298+ | ($fail.message // null) as $msg
288299 | (
289300 if $seg.failed then
290301 if ($msg // "" | startswith("=== Expected:")) then "fail"
@@ -300,7 +311,11 @@ jq -n \
300311 test_code: ($src.test_code // ""),
301312 }
302313 + (if $src.task_id then {task_id: $src.task_id} else {} end)
303- + (if $seg.failed then {message: ($msg // "test failed")} else {} end)
314+ + (if $seg.failed then
315+ {message: (if $msg == null then "test failed"
316+ elif ($fail.location // "") == "" then $msg
317+ else $fail.location + "\n" + $msg end)}
318+ else {} end)
304319 + (if $seg.output != "" then {output: ($seg.output[0:500])} else {} end)
305320 )
306321 | (if all(.status == "pass") then "pass" else "fail" end) as $top
0 commit comments