2828slug=" $1 "
2929solution_dir=$( realpath " ${2%/ } " )
3030output_dir=$( realpath " ${3%/ } " )
31- mkdir -p " $output_dir "
31+ mkdir -p " ${ output_dir} "
3232results_file=" ${output_dir} /results.json"
3333canonical_root=" /opt/test-runner/tests/${slug} "
3434test_file=" ${solution_dir} /${slug} /${slug} -tests.factor"
3535
3636echo " ${slug} : testing..."
3737
38- if [[ ! -f " $test_file " ]]; then
38+ if [[ ! -f " ${ test_file} " ]]; then
3939 jq -n --arg msg " test file not found: ${slug} -tests.factor" \
40- ' {version: 3, status: "error", message: $msg}' > " $ results_file"
40+ ' {version: 3, status: "error", message: $msg}' > " ${ results_file} "
4141 exit 0
4242fi
4343
4444# Copy the fixture to a fresh temp dir so the rewrite below does not mutate
4545# the source.
4646tmp_dir=$( mktemp -d -t " factor-runner-${slug} -XXXXX" )
47- trap ' rm -rf "$tmp_dir"' EXIT
48- cp -r " ${solution_dir} /." " $tmp_dir "
47+ trap ' rm -rf "${ tmp_dir} "' EXIT
48+ cp -r " ${solution_dir} /." " ${ tmp_dir} "
4949stripped_tests=" ${tmp_dir} /${slug} /${slug} -tests.factor"
50- awk ' !/^STOP-HERE$/' " $stripped_tests " > " ${stripped_tests} .new"
51- mv " ${stripped_tests} .new" " $stripped_tests "
50+ awk ' !/^STOP-HERE$/' " ${ stripped_tests} " > " ${stripped_tests} .new"
51+ mv " ${stripped_tests} .new" " ${ stripped_tests} "
5252
5353# Run Factor; capture combined stdout/stderr.
5454set +e
55- raw_output=$( cd " $tmp_dir " && factor -roots=. -run=exercism-tools " $slug " 2>&1 )
55+ raw_output=$( cd " ${ tmp_dir} " && factor -roots=. -run=exercism-tools " ${ slug} " 2>&1 )
5656set -e
5757# Normalize the tmp path to the canonical Docker path.
58- raw_output=${raw_output// $tmp_dir / $ canonical_root }
58+ raw_output=${raw_output// ${ tmp_dir} / ${ canonical_root} }
5959
6060# Awk parser shared by all stages: JSON-escape a single string field.
61- read -r -d ' ' AWK_JSON << 'AWK ' || true
61+ read -r -d ' ' awk_json << 'AWK ' || true
6262function json_str(s, r) {
6363 r = s
6464 gsub(/\\/, "\\\\", r)
7575# 1. Extract source-test records (one JSON object per line, NDJSON):
7676# {"line_no":N,"task_id":N|null,"test_code":"..."}
7777# Reads the post-strip file so line numbers match what Factor reports.
78- src_tests=$( awk " $AWK_JSON " '
78+ src_tests=$( awk " ${awk_json} " '
7979 BEGIN { task = "null" }
8080 /^[[:space:]]*TASK:[[:space:]]+[0-9]+/ {
8181 match($0, /TASK:[[:space:]]+[0-9]+/)
@@ -95,19 +95,21 @@ src_tests=$(awk "$AWK_JSON"'
9595# 2. Parse Factor stdout into NDJSON segments and failures:
9696# segments: {"type":"segment","idx":N,"failed":bool,"output":"..."}
9797# failures: {"type":"failure","line_no":N,"message":"..."}
98- parsed=$( printf ' %s\n' " $raw_output " | awk " $AWK_JSON " '
98+ parsed=$( printf ' %s\n' " ${ raw_output} " | awk " ${awk_json} " '
9999 function close_segment( out, i) {
100100 if (idx == 0) return
101101 out = ""
102102 for (i = 1; i <= seg_n; i++) out = out (i > 1 ? "\n" : "") seg[i]
103- sub(/^\n+/, "", out); sub(/\n+$/, "", out)
103+ sub(/^\n+/, "", out)
104+ sub(/\n+$/, "", out)
104105 printf "{\"type\":\"segment\",\"idx\":%d,\"failed\":%s,\"output\":%s}\n",
105106 idx, (seg_failed ? "true" : "false"), json_str(out)
106107 }
107108 function close_failure( body, i) {
108109 body = ""
109110 for (i = 1; i <= fail_n; i++) body = body (i > 1 ? "\n" : "") fail[i]
110- sub(/^\n+/, "", body); sub(/\n+$/, "", body)
111+ sub(/^\n+/, "", body)
112+ sub(/\n+$/, "", body)
111113 printf "{\"type\":\"failure\",\"line_no\":%d,\"message\":%s}\n",
112114 fail_line, json_str(body)
113115 }
@@ -122,65 +124,94 @@ parsed=$(printf '%s\n' "$raw_output" | awk "$AWK_JSON"'
122124 # must-infer → "Must Infer:"
123125 # must-infer-as → "Must Infer As:"
124126 BEGIN {
125- state = "inline"; idx = 0
127+ state = "inline"
128+ idx = 0
126129 header_re = "^(Unit Test|Unit Test~|Unit Test V~|Long Unit Test|Must Fail|Must Fail With|Must Not Fail|Must Infer|Must Infer As): "
127130 }
128131 state == "inline" && $0 ~ header_re {
129132 close_segment()
130- idx++; seg_failed = 0; seg_n = 0; delete seg
133+ idx++
134+ seg_failed = 0
135+ seg_n = 0
136+ delete seg
131137 next
132138 }
133139 state == "inline" && $0 == "###FAIL_BEGIN###" {
134- close_segment(); idx = 0
135- state = "fail_loc"; next
140+ close_segment()
141+ idx = 0
142+ state = "fail_loc"
143+ next
136144 }
137145 state == "inline" && $0 == "--> test failed!" {
138- seg_failed = 1; next
146+ seg_failed = 1
147+ next
139148 }
140149 state == "inline" {
141- if (idx > 0) { seg_n++; seg[seg_n] = $0 }
150+ if (idx > 0) {
151+ seg_n++
152+ seg[seg_n] = $0
153+ }
142154 next
143155 }
144156 state == "fail_loc" {
145157 if (match($0, /:[[:space:]]*[0-9]+[[:space:]]*$/)) {
146- n = substr($0, RSTART, RLENGTH); gsub(/[^0-9]/, "", n)
158+ n = substr($0, RSTART, RLENGTH)
159+ gsub(/[^0-9]/, "", n)
147160 fail_line = n + 0
148- } else { fail_line = 0 }
149- fail_n = 0; delete fail
150- state = "fail_body"; next
161+ } else {
162+ fail_line = 0
163+ }
164+ fail_n = 0
165+ delete fail
166+ state = "fail_body"
167+ next
151168 }
152169 state == "fail_body" && $0 == "###FAIL_END###" {
153170 close_failure()
154- state = "fail_between"; next
171+ state = "fail_between"
172+ next
155173 }
156174 state == "fail_body" {
157- fail_n++; fail[fail_n] = $0; next
175+ fail_n++
176+ fail[fail_n] = $0
177+ next
158178 }
159179 state == "fail_between" && $0 == "###FAIL_BEGIN###" {
160- state = "fail_loc"; next
180+ state = "fail_loc"
181+ next
161182 }
162183 END { close_segment() }
163184' )
164185
165- segments=$( printf ' %s\n' " $parsed " | awk ' /"type":"segment"/' || true)
166- failures=$( printf ' %s\n' " $parsed " | awk ' /"type":"failure"/' || true)
186+ segments=$( printf ' %s\n' " ${ parsed} " | awk ' /"type":"segment"/' || true)
187+ failures=$( printf ' %s\n' " ${ parsed} " | awk ' /"type":"failure"/' || true)
167188
168189# 3. If no segments emitted, surface a top-level error from the raw output.
169- if [[ -z " $segments " ]]; then
170- cleaned=$( printf ' %s\n' " $raw_output " | awk ' /^\([UO]\) /{exit} {print}' \
171- | awk ' NF { print; blank = 0; next } !blank { print; blank = 1 }' )
172- if [[ -z " $cleaned " ]]; then cleaned=" No tests were executed" ; fi
173- jq -n --arg msg " $cleaned " ' {version:3, status:"error", message:$msg}' > " $results_file "
190+ if [[ -z " ${segments} " ]]; then
191+ cleaned=$( printf ' %s\n' " ${raw_output} " | awk ' /^\([UO]\) /{exit} {print}' \
192+ | awk '
193+ NF {
194+ print
195+ blank = 0
196+ next
197+ }
198+ !blank {
199+ print
200+ blank = 1
201+ }
202+ ' )
203+ [[ -z " ${cleaned} " ]] && cleaned=" No tests were executed"
204+ jq -n --arg msg " ${cleaned} " ' {version:3, status:"error", message:$msg}' > " ${results_file} "
174205 exit 0
175206fi
176207
177208# 4. Compose the v3 JSON.
178209# --slurpfile would require files; instead pass NDJSON via --argjson after
179210# converting each line. We use jq -s on a pipeline of NDJSON inputs.
180211jq -n \
181- --argjson srcs " $( printf ' %s\n' " $src_tests " | jq -s ' .' ) " \
182- --argjson segs " $( printf ' %s\n' " $segments " | jq -s ' .' ) " \
183- --argjson fails " $( printf ' %s\n' " $failures " | jq -s ' .' ) " \
212+ --argjson srcs " $( printf ' %s\n' " ${ src_tests} " | jq -s ' .' ) " \
213+ --argjson segs " $( printf ' %s\n' " ${ segments} " | jq -s ' .' ) " \
214+ --argjson fails " $( printf ' %s\n' " ${ failures} " | jq -s ' .' ) " \
184215 '
185216 ($fails | map({(.line_no|tostring): .message}) | add // {}) as $fail_by_line
186217 | $segs | sort_by(.idx)
@@ -209,6 +240,6 @@ jq -n \
209240 )
210241 | (if all(.status == "pass") then "pass" else "fail" end) as $top
211242 | {version: 3, status: $top, tests: .}
212- ' > " $ results_file"
243+ ' > " ${ results_file} "
213244
214245echo " ${slug} : done"
0 commit comments