11#! /usr/bin/env sh
22
33# Synopsis:
4- # Run the test runner on a solution.
5-
4+ # Run the Exercism V test runner on a solution.
5+ #
66# Arguments:
7- # $1: exercise slug
8- # $2: path to solution folder
9- # $3: path to output directory
10-
7+ # $1: exercise slug
8+ # $2: path to solution folder
9+ # $3: path to output directory
10+ #
1111# Output:
12- # Writes the test results to a results .json file in the passed-in output directory.
13- # The test results are formatted according to the specifications at https://github.com/exercism/docs/blob/main/building/tooling/test-runners/interface.md
12+ # Writes a v2 results.json to the output directory, per
13+ # https://github.com/exercism/docs/blob/main/building/tooling/test-runners/interface.md
1414
1515# Example:
1616# ./bin/run.sh two-fer path/to/solution/folder/ path/to/output/directory/
@@ -33,26 +33,99 @@ echo "${slug}: testing..."
3333
3434cd " ${solution_dir} " > /dev/null
3535
36- # Run the tests for the provided implementation file and redirect stdout and
37- # stderr to capture it
3836test_output=$( v -stats test run_test.v 2>&1 )
3937exit_code=$?
4038
4139cd - > /dev/null
4240
43- # Write the results.json file based on the exit code of the command that was
44- # just executed that tested the implementation file
41+ # Strip the solution directory prefix from paths so output is portable,
42+ # and remove ANSI color codes (V emits them even when captured)
43+ test_output=$( printf ' %s' " ${test_output} " | sed -e " s#${solution_dir} /\{0,1\}##g" -e ' s/\x1b\[[0-9;]*m//g' )
44+
45+ # ---------- Error case (compile failure) ----------
46+ if [ ${exit_code} -ne 0 ] && echo " ${test_output} " | grep -q " error:" ; then
47+ jq -n --arg output " ${test_output} " \
48+ ' {version: 2, status: "error", message: $output}' > " ${results_file} "
49+ echo " ${slug} : done"
50+ exit 0
51+ fi
52+
53+ # ---------- Extract test_code from the test file (in source order) ----------
54+ # Walks run_test.v line by line. When a "fn test_xxx() {" line is found,
55+ # collects subsequent lines (unindented) as the test body until "}".
56+ test_codes_json=$( jq -Rs '
57+ split("\n") |
58+ reduce .[] as $line (
59+ {inside_test: false, name: "", body: "", result: []};
60+ if .inside_test then
61+ if ($line | test("^}")) then
62+ .result += [{name: .name, test_code: .body}] |
63+ .inside_test = false | .name = "" | .body = ""
64+ else
65+ .body += (if .body != "" then "\n" else "" end)
66+ + ($line | ltrimstr("\t"))
67+ end
68+ elif ($line | test("^fn test_")) then
69+ .inside_test = true |
70+ .name = ($line | capture("fn (?<n>test_[a-zA-Z0-9_]+)") | .n)
71+ else . end
72+ ) | .result
73+ ' " ${solution_dir} /run_test.v" )
74+
75+ # ---------- Parse V test output into per-test results ----------
76+ # V prints one "OK" or "FAIL" line per test with the pattern:
77+ # OK [1/9]... | main.test_xxx()
78+ # FAIL [1/9]... | main.test_xxx()
79+ # Lines between results (file:line, assert expression) are accumulated
80+ # as the failure message for the next FAIL.
81+ test_results_json=$( printf ' %s' " ${test_output} " | jq -Rs '
82+ def test_name: capture("main\\.(?<n>[^(]+)") | .n;
83+ split("\n") |
84+ reduce .[] as $line (
85+ {pending: "", tests: []};
86+ if ($line | test("OK.*\\| *main\\.")) then
87+ .tests += [{
88+ name: ($line | test_name),
89+ status: "pass"
90+ }] | .pending = ""
91+ elif ($line | test("FAIL.*\\| *main\\.")) then
92+ .tests += [{
93+ name: ($line | test_name),
94+ status: "fail",
95+ message: .pending
96+ }] | .pending = ""
97+ elif ($line | test("^---- Testing|^running tests in:|Summary for|^--------|^Failed command|compilation |generated |V source")) then
98+ .
99+ else
100+ .pending += (if .pending != "" then "\n" + $line else $line end)
101+ end
102+ ) | .tests
103+ ' )
104+
105+ # ---------- Merge in source file order ----------
106+ # Use test_codes order (= source file order) as the authority.
107+ # For each test_code entry, find the matching result by name.
108+ tests_json=$( jq -n \
109+ --argjson codes " ${test_codes_json} " \
110+ --argjson results " ${test_results_json} " \
111+ ' [ $codes[] | . as $c |
112+ ($results[] | select(.name == $c.name)) // {status: "error", message: "Test did not run."}
113+ | . + $c
114+ ]' )
115+
116+ # ---------- Determine overall status ----------
45117if [ ${exit_code} -eq 0 ]; then
46- jq -n ' {version: 1, status: "pass"} ' > ${results_file}
118+ overall= " pass"
47119else
48- echo " ${test_output} " | grep -q " error:"
49- if [ $? -eq 0 ]; then
50- status=" error"
51- else
52- status=" fail"
53- fi
54-
55- jq -n --arg output " ${test_output} " --arg status " ${status} " ' {version: 1, status: $status, message: $output}' > ${results_file}
120+ overall=" fail"
56121fi
57122
123+ # ---------- Assemble results and truncate output fields to 500 chars ----------
124+ jq -n --arg status " ${overall} " --argjson tests " ${tests_json} " '
125+ def trunc: if length > 500 then .[:481] + " [output truncated]" else . end;
126+ {version: 2, status: $status, tests: ($tests | map(
127+ if .output then .output |= trunc else . end
128+ ))}
129+ ' > " ${results_file} "
130+
58131echo " ${slug} : done"
0 commit comments