Skip to content

Commit 0a5f11b

Browse files
committed
initial test-runner, with tests
1 parent 0b6f93b commit 0a5f11b

32 files changed

Lines changed: 393 additions & 58 deletions

Dockerfile

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,40 @@
1-
FROM alpine:3.18
1+
FROM ubuntu:24.04
22

3-
# install packages required to run the tests
4-
RUN apk add --no-cache jq coreutils
3+
ENV LUA_VER="5.4.8"
4+
ENV LUA_CHECKSUM="4f18ddae154e793e46eeab727c59ef1c0c0c2b744e7b94219710d76f530629ae"
5+
ENV LUAROCKS_VER="3.12.0"
6+
ENV LUAROCKS_GPG_KEY="3FD8F43C2BB3C478"
57

8+
RUN apt-get update && \
9+
apt-get install -y curl gcc jq make unzip gnupg git && \
10+
rm -rf /var/lib/apt/lists/* && \
11+
apt-get purge --auto-remove && \
12+
apt-get clean
13+
14+
RUN curl -R -O -L http://www.lua.org/ftp/lua-${LUA_VER}.tar.gz && \
15+
[ "$(sha256sum lua-${LUA_VER}.tar.gz | cut -d' ' -f1)" = "${LUA_CHECKSUM}" ] && \
16+
tar -zxf lua-${LUA_VER}.tar.gz && \
17+
cd lua-${LUA_VER} && \
18+
make all install && \
19+
cd .. && \
20+
rm lua-${LUA_VER}.tar.gz && \
21+
rm -rf lua-${LUA_VER}
22+
23+
RUN curl -R -O -L https://luarocks.org/releases/luarocks-${LUAROCKS_VER}.tar.gz && \
24+
curl -R -O -L https://luarocks.org/releases/luarocks-${LUAROCKS_VER}.tar.gz.asc && \
25+
gpg --keyserver keyserver.ubuntu.com --recv-keys ${LUAROCKS_GPG_KEY} && \
26+
gpg --verify luarocks-${LUAROCKS_VER}.tar.gz.asc luarocks-${LUAROCKS_VER}.tar.gz && \
27+
tar -zxpf luarocks-${LUAROCKS_VER}.tar.gz && \
28+
cd luarocks-${LUAROCKS_VER} && \
29+
./configure && make && make install && \
30+
cd .. && \
31+
rm luarocks-${LUAROCKS_VER}.tar.gz.asc && \
32+
rm luarocks-${LUAROCKS_VER}.tar.gz && \
33+
rm -rf luarocks-${LUAROCKS_VER}
34+
35+
RUN luarocks install busted
36+
RUN luarocks install moonscript
37+
38+
COPY . /opt/test-runner
639
WORKDIR /opt/test-runner
7-
COPY . .
8-
ENTRYPOINT ["/opt/test-runner/bin/run.sh"]
40+
ENTRYPOINT ["/opt/test-runner/bin/run.moon"]

bin/run-in-docker.sh

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
#!/usr/bin/env sh
1+
#!/usr/bin/env bash
2+
set -e
23

34
# Synopsis:
45
# Run the test runner on a solution using the test runner Docker image.
56
# The test runner Docker image is built automatically.
67

78
# Arguments:
89
# $1: exercise slug
9-
# $2: path to solution folder
10-
# $3: path to output directory
10+
# $2: absolute path to solution folder
11+
# $3: absolute path to output directory
1112

1213
# Output:
1314
# Writes the test results to a results.json file in the passed-in output directory.
1415
# The test results are formatted according to the specifications at https://github.com/exercism/docs/blob/main/building/tooling/test-runners/interface.md
1516

1617
# Example:
17-
# ./bin/run-in-docker.sh two-fer path/to/solution/folder/ path/to/output/directory/
18-
19-
# Stop executing when a command returns a non-zero return code
20-
set -e
18+
# ./bin/run-in-docker.sh two-fer /absolute/path/to/two-fer/solution/folder/ /absolute/path/to/output/directory/
2119

2220
# If any required arguments is missing, print the usage and exit
2321
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
24-
echo "usage: ./bin/run-in-docker.sh exercise-slug path/to/solution/folder/ path/to/output/directory/"
22+
echo "usage: ./bin/run-in-docker.sh exercise-slug /absolute/path/to/solution/folder/ /absolute/path/to/output/directory/"
2523
exit 1
2624
fi
2725

@@ -43,4 +41,4 @@ docker run \
4341
--mount type=bind,src="${solution_dir}",dst=/solution \
4442
--mount type=bind,src="${output_dir}",dst=/output \
4543
--mount type=tmpfs,dst=/tmp \
46-
exercism/moonscript-test-runner "${slug}" /solution /output
44+
exercism/moonscript-test-runner "${slug}" /solution /output

bin/run-tests-in-docker.sh

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
#!/usr/bin/env sh
1+
#!/usr/bin/env bash
2+
set -e
23

34
# Synopsis:
45
# Test the test runner Docker image by running it against a predefined set of
@@ -12,9 +13,6 @@
1213
# Example:
1314
# ./bin/run-tests-in-docker.sh
1415

15-
# Stop executing when a command returns a non-zero return code
16-
set -e
17-
1816
# Build the Docker image
1917
docker build --rm -t exercism/moonscript-test-runner .
2018

bin/run-tests.sh

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
#!/usr/bin/env sh
1+
#! /bin/sh
22

33
# Synopsis:
44
# Test the test runner by running it against a predefined set of solutions
@@ -17,21 +17,14 @@ exit_code=0
1717
for test_dir in tests/*; do
1818
test_dir_name=$(basename "${test_dir}")
1919
test_dir_path=$(realpath "${test_dir}")
20-
21-
bin/run.sh "${test_dir_name}" "${test_dir_path}" "${test_dir_path}"
22-
23-
# OPTIONAL: Normalize the results file
24-
# If the results.json file contains information that changes between
25-
# different test runs (e.g. timing information or paths), you should normalize
26-
# the results file to allow the diff comparison below to work as expected
27-
28-
file="results.json"
29-
expected_file="expected_${file}"
30-
echo "${test_dir_name}: comparing ${file} to ${expected_file}"
31-
32-
if ! diff "${test_dir_path}/${file}" "${test_dir_path}/${expected_file}"; then
33-
exit_code=1
34-
fi
20+
results_file="results.json"
21+
results_file_path="${test_dir}/${results_file}"
22+
expected_results_file="expected_results.json"
23+
expected_results_file_path="${test_dir}/${expected_results_file}"
24+
25+
bin/run.moon "${test_dir_name}" "${test_dir}" "${test_dir}" \
26+
&& bin/test-result-compare.lua "${results_file_path}" "${expected_results_file_path}" \
27+
|| exit_code=1
3528
done
3629

3730
exit ${exit_code}

bin/run.moon

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#! /usr/bin/env moon
2+
3+
require 'moonscript'
4+
lfs = require 'lfs'
5+
json = (require 'dkjson').use_lpeg!
6+
getopt = require 'alt_getopt'
7+
local verbose
8+
9+
import p from require 'moon'
10+
11+
12+
-- -----------------------------------------------------------
13+
show_help = (args) ->
14+
print "Usage: #{args[0]} [-h] [-v] slug solution-dir output-dir"
15+
print "Where: -h show this help"
16+
print " -v verbose: show the output JSON"
17+
os.exit!
18+
19+
20+
-- -----------------------------------------------------------
21+
file_exists = (path) ->
22+
attrs = lfs.attributes path
23+
not not attrs
24+
25+
is_directory = (path) ->
26+
attrs = lfs.attributes path
27+
attrs and attrs.mode == 'directory'
28+
29+
realpath = (path) ->
30+
fh = io.popen "realpath #{path}"
31+
dir = fh\read!
32+
fh\close!
33+
dir
34+
35+
validate = (args) ->
36+
show_help args unless #args == 3
37+
{slug, src_dir, dest_dir} = args
38+
assert slug != '', 'First arg, the slug, cannot be empty'
39+
assert is_directory(src_dir), 'Second arg, the solution directory, must be a directory'
40+
assert is_directory(dest_dir), 'Second arg, the output directory, must be a directory'
41+
42+
slug, realpath(src_dir), realpath(dest_dir)
43+
44+
45+
-- -----------------------------------------------------------
46+
run_tests = (slug, dir) ->
47+
ok, err = lfs.chdir dir
48+
assert ok, err
49+
50+
-- unskip tests
51+
cmd = "perl -i.bak -pe 's{^\\s*\\Kpending\\b}{it}' *_spec.moon"
52+
ok, result_type, status = os.execute cmd
53+
assert ok
54+
55+
-- launch `busted`
56+
fh = io.popen 'busted -o json', 'r'
57+
json_output = fh\read 'a'
58+
ok, exit_type, exit_status = fh\close!
59+
60+
if exit_type == 'signal'
61+
return {
62+
status: 'error',
63+
message: json_output
64+
}
65+
66+
data = json.decode json_output
67+
68+
if not data
69+
return {
70+
status: 'error',
71+
message: json_output
72+
}
73+
74+
if exit_status != 0 and #data.successes == 0 and #data.failures == 0 and #data.errors > 0
75+
return {
76+
status: 'error',
77+
message: data.errors[1].message
78+
}
79+
80+
results = {}
81+
82+
for test in *data.successes
83+
results[test.element.name] = {
84+
status: 'pass',
85+
name: test.element.name,
86+
}
87+
88+
for test in *data.failures
89+
results[test.element.name] = {
90+
status: 'fail',
91+
name: test.element.name,
92+
message: test.trace.message,
93+
}
94+
95+
results
96+
97+
98+
-- -----------------------------------------------------------
99+
get_test_bodies = (slug, dir) ->
100+
ok, err = lfs.chdir dir
101+
assert ok, err
102+
103+
order = {}
104+
bodies = {}
105+
106+
test_file = "#{slug\gsub('-', '_')}_spec.moon"
107+
return unless file_exists test_file -- let `busted` handle the error messaging
108+
109+
fh = io.open test_file, 'r'
110+
111+
pattern = (word) -> '^%s+' .. word .. '%s+[\'"](.+)[\'"],%s+->'
112+
patterns = it: pattern('it'), pending: pattern('pending')
113+
114+
local test_name
115+
test_body = {}
116+
in_test = false
117+
118+
for line in fh\lines!
119+
if line\match '^%s+describe '
120+
if test_name
121+
bodies[test_name] = table.concat test_body, '\n'
122+
test_body = {}
123+
test_name = nil
124+
in_test = false
125+
126+
m = line\match(patterns.it) or line\match(patterns.pending)
127+
if not m
128+
if in_test
129+
table.insert test_body, line
130+
else
131+
table.insert order, m
132+
if in_test
133+
bodies[test_name] = table.concat test_body, '\n'
134+
test_body = {}
135+
test_name = m
136+
in_test = true
137+
138+
fh\close!
139+
bodies[test_name] = table.concat test_body, '\n'
140+
order, bodies
141+
142+
143+
-- -----------------------------------------------------------
144+
write_results = (slug, test_results, names, bodies, dir) ->
145+
ok, err = lfs.chdir dir
146+
assert ok, "#{err}: #{dir}"
147+
148+
results = version: 2, status: nil, tests: {}
149+
150+
if test_results.status
151+
-- this was an error result
152+
results.status = test_results.status
153+
results.message = test_results.message
154+
155+
else
156+
status = 'pass'
157+
for name in *names
158+
test = test_results[name]
159+
assert test, "no test result for #{name}"
160+
status = 'fail' if test.status == 'fail'
161+
test.test_code = bodies[name]
162+
table.insert results.tests, test
163+
results.status = status
164+
165+
fh = io.open 'results.json', 'w'
166+
fh\write (json.encode results) .. '\n'
167+
fh\close!
168+
169+
os.execute "jq . results.json" if verbose
170+
171+
172+
-- -----------------------------------------------------------
173+
main = (args) ->
174+
opts,optind = getopt.get_opts args, 'hv', {}
175+
176+
show_help args if opts.h
177+
verbose = not not opts.v
178+
table.remove args, 1 for _ = 1, optind - 1
179+
180+
slug, src_dir, dest_dir = validate args
181+
182+
print "#{slug}: testing ..."
183+
184+
test_names_ordered, test_code = get_test_bodies slug, src_dir
185+
186+
test_results = run_tests slug, src_dir
187+
188+
write_results slug, test_results, test_names_ordered, test_code, dest_dir
189+
190+
print "#{slug}: ... done"
191+
192+
main arg

bin/test-result-compare.lua

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env lua
2+
3+
local json = require('dkjson')
4+
local tablex = require('pl.tablex')
5+
local pretty = require('pl.pretty')
6+
7+
local function load_json(file)
8+
local fd <close> = io.open(file)
9+
return json.decode(fd:read('a'))
10+
end
11+
12+
local result = load_json(arg[1])
13+
local expected = load_json(arg[2])
14+
15+
if not tablex.deepcompare(result, expected) then
16+
print('\n========[ RESULT ]========\n')
17+
print(pretty.write(result))
18+
19+
print('\n========[ EXPECTED ]========\n')
20+
print(pretty.write(expected))
21+
22+
os.exit(1)
23+
end
24+
25+
os.exit(0)

tests/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
*.bak
2+
results.json

tests/all-fail/expected_results.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/empty-file/expected_results.json

Lines changed: 0 additions & 5 deletions
This file was deleted.

tests/example-all-fail/.busted

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
return {
2+
default = {
3+
ROOT = { '.' }
4+
}
5+
}

0 commit comments

Comments
 (0)