Skip to content

Commit c61794a

Browse files
committed
Upgrade to version 2 spec
1 parent 976ed67 commit c61794a

File tree

11 files changed

+459
-30
lines changed

11 files changed

+459
-30
lines changed

bin/parse_tests.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import re
5+
import sys
6+
from enum import Enum
7+
8+
9+
class ExitCode(Enum):
10+
PASS = 0
11+
FAIL = 1
12+
ERROR = 2
13+
14+
15+
class Status(Enum):
16+
PASS = "pass"
17+
FAIL = "fail"
18+
ERROR = "error"
19+
20+
21+
TEST_FUNCTION = "ert-deftest"
22+
TEST_FAILED_FUNCTION = "ert-test-failed"
23+
24+
25+
def parse_test_functions(s: str):
26+
"""
27+
Retrieve test function names and code from regions like
28+
'(ert-deftest name-is-persistent
29+
"Test that robot name is persistent."
30+
(should (equal (robot-name *robbie*)
31+
(robot-name *robbie*))))'
32+
in the test file
33+
"""
34+
function_matches = re.finditer(
35+
fr"\({TEST_FUNCTION}\s+(?P<name>[\w-]+)\s+\(\)\s*(?P<docstring>\".*\")?\s*(?P<code>(?:\n.+)+)\)",
36+
s,
37+
)
38+
names = []
39+
code_pieces = []
40+
for m in function_matches:
41+
names.append(m["name"])
42+
code_pieces.append(m["code"].strip())
43+
return names, code_pieces
44+
45+
46+
def parse_test_statuses(s: str):
47+
"""
48+
Retrieve test statuses from lines like
49+
'passed 3/4 name-is-persistent (0.000049 sec)'
50+
in the test output
51+
"""
52+
status_matches = re.finditer(
53+
r"(?P<status>passed|FAILED)\s+(?P<number>\d+)\/\d+\s+(?P<name>[\w-]+)\s*(?:\(\d+\.\d+\ssec\))?",
54+
s,
55+
)
56+
return {
57+
m["name"]: (
58+
Status.PASS if m["status"].strip() == "passed" else Status.FAIL
59+
)
60+
for m in status_matches
61+
}
62+
63+
64+
def parse_test_message(name: str, s: str):
65+
"""
66+
Retrieve test messages from regions like
67+
'Test name-can-be-reset condition:
68+
(wrong-type-argument hash-table-p nil)
69+
FAILED 2/4 name-can-be-reset'
70+
in the test output
71+
"""
72+
condition_matches = re.finditer(
73+
fr"Test\s{name}\scondition:\s+(?P<condition>\((?P<function>.+)(?:\n.+)+)FAILED\s+(?P<number>\d+)/\d+\s+{name}",
74+
s,
75+
)
76+
try:
77+
cond_match = next(condition_matches)
78+
except StopIteration:
79+
return None, None
80+
message = cond_match["condition"].strip()
81+
# status is 'fail' if test condition starts with the test failed function
82+
# otherwise there is an error
83+
status = (
84+
Status.FAIL
85+
if cond_match["function"] == TEST_FAILED_FUNCTION
86+
else Status.ERROR
87+
)
88+
return message, status
89+
90+
91+
def parse_test_output(name: str, num: int, s: str):
92+
"""
93+
Retrieve test outputs from regions like
94+
'Running 4 tests (2022-01-04 17:06:51+0200, selector ‘t’)
95+
"1DG190"
96+
passed 1/4 different-robots-have-different-names (0.000075 sec)'
97+
,
98+
' passed 1/4 different-robots-have-different-names (0.000075 sec)
99+
"1XW454"
100+
passed 2/4 name-can-be-reset (0.000047 sec)'
101+
and
102+
' passed 3/4 name-is-persistent (0.000049 sec)
103+
"1DG190"
104+
Test name-matches-expected-pattern backtrace:'
105+
in the test output
106+
"""
107+
status_line_regexp = fr"(?:passed|FAILED)\s+{num - 1}\/\d+\s+(?:[\w-]+)\s*(?:\(\d+\.\d+\ssec\))?"
108+
output_regexp = fr"(?P<output>(?:\n.*)+)\s*(?:passed\s+{num}|Test\s{name}\sbacktrace)"
109+
output_matches = re.finditer(
110+
(r"\)" if num == 1 else status_line_regexp) + output_regexp, s
111+
)
112+
try:
113+
output_match = next(output_matches)
114+
except StopIteration:
115+
return None, None
116+
output = output_match["output"].strip()
117+
message = None
118+
# Output is limited to 500 chars
119+
if len(output) > 500:
120+
message = "Output was truncated. Please limit to 500 chars"
121+
output = output[:500]
122+
return output, message
123+
124+
125+
def run(test_file_path: str, test_output_file_path: str):
126+
exit_code = ExitCode.PASS
127+
with open(test_file_path, encoding="utf-8") as f:
128+
test_file_content = f.read()
129+
with open(test_output_file_path, encoding="utf-8") as f:
130+
test_output_file_content = f.read()
131+
names, code_pieces = parse_test_functions(test_file_content)
132+
name_to_number = {name: i + 1 for i, name in enumerate(sorted(names))}
133+
name_to_status = parse_test_statuses(test_output_file_content)
134+
status_to_exit_code = {Status(ec.name.lower()): ec for ec in ExitCode}
135+
tests = []
136+
for name, code in zip(names, code_pieces):
137+
test = {}
138+
number = name_to_number[name]
139+
test["name"] = name
140+
test["test_code"] = code.strip()
141+
# get status from status line or assume it is syntax error if there is no one
142+
status = name_to_status.get(name, Status.ERROR)
143+
message = None
144+
condition_message, message_status = parse_test_message(
145+
name, test_output_file_content
146+
)
147+
if condition_message:
148+
message, status = condition_message, message_status
149+
output, output_message = parse_test_output(
150+
name, int(number), test_output_file_content
151+
)
152+
if output_message and status != Status.PASS:
153+
if message:
154+
message += "\n" + output_message
155+
else:
156+
message = output_message
157+
exit_code = max(
158+
exit_code, status_to_exit_code[status], key=lambda x: x.value
159+
)
160+
test["status"] = status.value
161+
test["message"] = message
162+
if output:
163+
test["output"] = output
164+
tests.append(test)
165+
print(json.dumps(tests))
166+
return exit_code
167+
168+
169+
if __name__ == "__main__":
170+
if len(sys.argv) < 3:
171+
print("./parse-tests.py <test-file> <test-output>", file=sys.stderr)
172+
sys.exit(ExitCode.ERROR.value)
173+
else:
174+
exit_code = run(*sys.argv[1:])
175+
sys.exit(exit_code.value)

bin/run.sh

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,11 @@ if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
2222
fi
2323

2424
slug="$1"
25+
script_dir="$(dirname "$0")"
2526
input_dir="${2%/}"
2627
output_dir="${3%/}"
2728
test_file="${input_dir}/${slug}-test.el"
29+
test_output_file="$(mktemp --suffix ".out")"
2830
results_file="${output_dir}/results.json"
2931

3032
# Create the output directory if it doesn't exist
@@ -34,24 +36,26 @@ echo "${slug}: testing..."
3436

3537
pushd "${input_dir}" > /dev/null
3638

37-
# Run the tests for the provided implementation file and redirect stdout and
38-
# stderr to capture it
39-
test_output=$(emacs -batch -l ert -l "${test_file}" -f ert-run-tests-batch-and-exit 2>&1)
40-
exit_code=$?
39+
# Run the tests for the provided implementation file and record all terminal
40+
# output to a temporary file to preserve output order
41+
script -q -c "emacs -batch -l ert -l \"${test_file}\" -f ert-run-tests-batch-and-exit" \
42+
"$test_output_file" &> /dev/null
4143

4244
popd > /dev/null
4345

44-
# Write the results.json file based on the exit code of the command that was
45-
# just executed that tested the implementation file
46-
if [ $exit_code -eq 0 ]; then
47-
jq -n '{version: 1, status: "pass"}' > ${results_file}
48-
else
49-
# Manually add colors to the output to help scanning the output for errors
50-
colorized_test_output=$(echo "${test_output}" \
51-
| GREP_COLOR='01;31' grep --color=always -E -e 'FAILED.*$|$' \
52-
| GREP_COLOR='01;32' grep --color=always -E -e 'passed.*$|$')
53-
54-
jq -n --arg output "${colorized_test_output}" '{version: 1, status: "fail", message: $output}' > ${results_file}
55-
fi
46+
# Write the results.json file based on both the exit code of the command that
47+
# was just executed that tested the implementation file and per-test information
48+
tests=$("$script_dir/parse_tests.py" "$test_file" "$test_output_file")
49+
exit_code=$?
50+
case $exit_code in
51+
0) status="pass" ;;
52+
1) status="fail" ;;
53+
2) status="error" ;;
54+
*) echo "'parse_tests.py' script returned unknown exit code" 1>&2 && exit 1 ;;
55+
esac
56+
57+
jq -n --arg status "$status" \
58+
--argjson tests "$tests" \
59+
'{version: 2, status: $status, message: null, tests: $tests}' > ${results_file}
5660

5761
echo "${slug}: done"

tests/example-all-fail/example-all-fail.el

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
(defun leap-year-p (year)
88
"Determine if YEAR is a leap year."
9-
(not ((and (= 0 (mod year 4))
9+
(not (and (= 0 (mod year 4))
1010
(or (not (= 0 (mod year 100)))
11-
(= 0 (mod year 401)))))))
11+
(= 0 (mod year 400))))))
1212

1313
(provide 'leap)
1414
;;; leap.el ends here
Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,37 @@
11
{
2-
"version": 1,
2+
"version": 2,
33
"status": "fail",
4-
"message": "Loading /solution/example-all-fail.el (source)...\nRunning 5 tests \nTest any-old-year backtrace:\n ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (mod ye\n (not ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (m\n leap-year-p(1997)\n apply(leap-year-p 1997)\n (setq value-7 (apply fn-5 args-6))\n (unwind-protect (setq value-7 (apply fn-5 args-6)) (setq form-descri\n (not (unwind-protect (setq value-7 (apply fn-5 args-6)) (setq form-d\n (if (not (unwind-protect (setq value-7 (apply fn-5 args-6)) (setq fo\n (let (form-description-9) (if (not (unwind-protect (setq value-7 (ap\n (let ((value-7 (quote ert-form-evaluation-aborted-8))) (let (form-de\n (let* ((fn-5 (function leap-year-p)) (args-6 (condition-case err (le\n (lambda nil (let* ((fn-5 (function leap-year-p)) (args-6 (condition-\n ert--run-test-internal(#s(ert--test-execution-info :test #s(ert-test\n ert-run-test(#s(ert-test :name any-old-year :documentation nil :body\n ert-run-or-rerun-test(#s(ert--stats :selector t :tests [#s(ert-test \n ert-run-tests(t #f(compiled-function (event-type &rest event-args) #\n ert-run-tests-batch(nil)\n ert-run-tests-batch-and-exit()\n command-line-1((\"-l\" \"ert\" \"-l\" \"/opt/test-runner/tests/example-all-\n command-line()\n normal-top-level()\nTest any-old-year condition:\n (invalid-function\n (and\n (= 0\n\t (mod year 4))\n (or\n (not\n\t(= 0 ...))\n (= 0\n\t (mod year 401)))))\n \u001b[01;31m\u001b[KFAILED 1/5 any-old-year\u001b[m\u001b[K\nTest century backtrace:\n ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (mod ye\n (not ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (m\n leap-year-p(1900)\n apply(leap-year-p 1900)\n (setq value-17 (apply fn-15 args-16))\n (unwind-protect (setq value-17 (apply fn-15 args-16)) (setq form-des\n (not (unwind-protect (setq value-17 (apply fn-15 args-16)) (setq for\n (if (not (unwind-protect (setq value-17 (apply fn-15 args-16)) (setq\n (let (form-description-19) (if (not (unwind-protect (setq value-17 (\n (let ((value-17 (quote ert-form-evaluation-aborted-18))) (let (form-\n (let* ((fn-15 (function leap-year-p)) (args-16 (condition-case err (\n (lambda nil (let* ((fn-15 (function leap-year-p)) (args-16 (conditio\n ert--run-test-internal(#s(ert--test-execution-info :test #s(ert-test\n ert-run-test(#s(ert-test :name century :documentation nil :body (lam\n ert-run-or-rerun-test(#s(ert--stats :selector t :tests [#s(ert-test \n ert-run-tests(t #f(compiled-function (event-type &rest event-args) #\n ert-run-tests-batch(nil)\n ert-run-tests-batch-and-exit()\n command-line-1((\"-l\" \"ert\" \"-l\" \"/opt/test-runner/tests/example-all-\n command-line()\n normal-top-level()\nTest century condition:\n (invalid-function\n (and\n (= 0\n\t (mod year 4))\n (or\n (not\n\t(= 0 ...))\n (= 0\n\t (mod year 401)))))\n \u001b[01;31m\u001b[KFAILED 2/5 century\u001b[m\u001b[K\nTest exceptional-century backtrace:\n ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (mod ye\n (not ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (m\n leap-year-p(2000)\n apply(leap-year-p 2000)\n (setq value-22 (apply fn-20 args-21))\n (unwind-protect (setq value-22 (apply fn-20 args-21)) (setq form-des\n (if (unwind-protect (setq value-22 (apply fn-20 args-21)) (setq form\n (let (form-description-24) (if (unwind-protect (setq value-22 (apply\n (let ((value-22 (quote ert-form-evaluation-aborted-23))) (let (form-\n (let* ((fn-20 (function leap-year-p)) (args-21 (condition-case err (\n (lambda nil (let* ((fn-20 (function leap-year-p)) (args-21 (conditio\n ert--run-test-internal(#s(ert--test-execution-info :test #s(ert-test\n ert-run-test(#s(ert-test :name exceptional-century :documentation ni\n ert-run-or-rerun-test(#s(ert--stats :selector t :tests [#s(ert-test \n ert-run-tests(t #f(compiled-function (event-type &rest event-args) #\n ert-run-tests-batch(nil)\n ert-run-tests-batch-and-exit()\n command-line-1((\"-l\" \"ert\" \"-l\" \"/opt/test-runner/tests/example-all-\n command-line()\n normal-top-level()\nTest exceptional-century condition:\n (invalid-function\n (and\n (= 0\n\t (mod year 4))\n (or\n (not\n\t(= 0 ...))\n (= 0\n\t (mod year 401)))))\n \u001b[01;31m\u001b[KFAILED 3/5 exceptional-century\u001b[m\u001b[K\nTest non-leap-even-year backtrace:\n ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (mod ye\n (not ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (m\n leap-year-p(1997)\n apply(leap-year-p 1997)\n (setq value-12 (apply fn-10 args-11))\n (unwind-protect (setq value-12 (apply fn-10 args-11)) (setq form-des\n (not (unwind-protect (setq value-12 (apply fn-10 args-11)) (setq for\n (if (not (unwind-protect (setq value-12 (apply fn-10 args-11)) (setq\n (let (form-description-14) (if (not (unwind-protect (setq value-12 (\n (let ((value-12 (quote ert-form-evaluation-aborted-13))) (let (form-\n (let* ((fn-10 (function leap-year-p)) (args-11 (condition-case err (\n (lambda nil (let* ((fn-10 (function leap-year-p)) (args-11 (conditio\n ert--run-test-internal(#s(ert--test-execution-info :test #s(ert-test\n ert-run-test(#s(ert-test :name non-leap-even-year :documentation nil\n ert-run-or-rerun-test(#s(ert--stats :selector t :tests [#s(ert-test \n ert-run-tests(t #f(compiled-function (event-type &rest event-args) #\n ert-run-tests-batch(nil)\n ert-run-tests-batch-and-exit()\n command-line-1((\"-l\" \"ert\" \"-l\" \"/opt/test-runner/tests/example-all-\n command-line()\n normal-top-level()\nTest non-leap-even-year condition:\n (invalid-function\n (and\n (= 0\n\t (mod year 4))\n (or\n (not\n\t(= 0 ...))\n (= 0\n\t (mod year 401)))))\n \u001b[01;31m\u001b[KFAILED 4/5 non-leap-even-year\u001b[m\u001b[K\nTest vanilla-leap-year backtrace:\n ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (mod ye\n (not ((and (= 0 (mod year 4)) (or (not (= 0 (mod year 100))) (= 0 (m\n leap-year-p(1996)\n apply(leap-year-p 1996)\n (setq value-2 (apply fn-0 args-1))\n (unwind-protect (setq value-2 (apply fn-0 args-1)) (setq form-descri\n (if (unwind-protect (setq value-2 (apply fn-0 args-1)) (setq form-de\n (let (form-description-4) (if (unwind-protect (setq value-2 (apply f\n (let ((value-2 (quote ert-form-evaluation-aborted-3))) (let (form-de\n (let* ((fn-0 (function leap-year-p)) (args-1 (condition-case err (le\n (lambda nil (let* ((fn-0 (function leap-year-p)) (args-1 (condition-\n ert--run-test-internal(#s(ert--test-execution-info :test #s(ert-test\n ert-run-test(#s(ert-test :name vanilla-leap-year :documentation nil \n ert-run-or-rerun-test(#s(ert--stats :selector t :tests [#s(ert-test \n ert-run-tests(t #f(compiled-function (event-type &rest event-args) #\n ert-run-tests-batch(nil)\n ert-run-tests-batch-and-exit()\n command-line-1((\"-l\" \"ert\" \"-l\" \"/opt/test-runner/tests/example-all-\n command-line()\n normal-top-level()\nTest vanilla-leap-year condition:\n (invalid-function\n (and\n (= 0\n\t (mod year 4))\n (or\n (not\n\t(= 0 ...))\n (= 0\n\t (mod year 401)))))\n \u001b[01;31m\u001b[KFAILED 5/5 vanilla-leap-year\u001b[m\u001b[K\n\nRan 5 tests, 0 results as expected, 5 unexpected \n\n5 unexpected results:\n \u001b[01;31m\u001b[KFAILED any-old-year\u001b[m\u001b[K\n \u001b[01;31m\u001b[KFAILED century\u001b[m\u001b[K\n \u001b[01;31m\u001b[KFAILED exceptional-century\u001b[m\u001b[K\n \u001b[01;31m\u001b[KFAILED non-leap-even-year\u001b[m\u001b[K\n \u001b[01;31m\u001b[KFAILED vanilla-leap-year\u001b[m\u001b[K"
4+
"message": null,
5+
"tests": [
6+
{
7+
"name": "vanilla-leap-year",
8+
"test_code": "(should (leap-year-p 1996))",
9+
"status": "fail",
10+
"message": "(ert-test-failed\n ((should\n (leap-year-p 1996))\n :form\n (leap-year-p 1996)\n :value nil))"
11+
},
12+
{
13+
"name": "any-old-year",
14+
"test_code": "(should-not (leap-year-p 1997))",
15+
"status": "fail",
16+
"message": "(ert-test-failed\n ((should-not\n (leap-year-p 1997))\n :form\n (leap-year-p 1997)\n :value t))"
17+
},
18+
{
19+
"name": "non-leap-even-year",
20+
"test_code": "(should-not (leap-year-p 1997))",
21+
"status": "fail",
22+
"message": "(ert-test-failed\n ((should-not\n (leap-year-p 1997))\n :form\n (leap-year-p 1997)\n :value t))"
23+
},
24+
{
25+
"name": "century",
26+
"test_code": "(should-not (leap-year-p 1900))",
27+
"status": "fail",
28+
"message": "(ert-test-failed\n ((should-not\n (leap-year-p 1900))\n :form\n (leap-year-p 1900)\n :value t))"
29+
},
30+
{
31+
"name": "exceptional-century",
32+
"test_code": "(should (leap-year-p 2000))",
33+
"status": "fail",
34+
"message": "(ert-test-failed\n ((should\n (leap-year-p 2000))\n :form\n (leap-year-p 2000)\n :value nil))"
35+
}
36+
]
537
}

0 commit comments

Comments
 (0)