Skip to content

Commit 2b8ba25

Browse files
committed
hello-world, with scripts and workflows
1 parent 56bcc05 commit 2b8ba25

14 files changed

Lines changed: 410 additions & 50 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: PR
2+
3+
on:
4+
pull_request:
5+
6+
jobs:
7+
test:
8+
runs-on: ubuntu-24.04
9+
10+
steps:
11+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
12+
13+
- uses: leafo/gh-actions-lua@8c9e175e7a3d77e21f809eefbee34a19b858641b
14+
with:
15+
luaVersion: 5.4
16+
17+
- uses: leafo/gh-actions-luarocks@97053c556d6ce2c8e26eb7ac93743437c7af7248
18+
19+
- name: build
20+
run: |
21+
luarocks install busted
22+
luarocks install moonscript
23+
24+
- name: Run tests for changed/added exercises
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
run: |
28+
pr_endpoint=$(jq -r '"repos/\(.repository.full_name)/pulls/\(.pull_request.number)"' "$GITHUB_EVENT_PATH")
29+
gh api "$pr_endpoint/files" --paginate --jq '
30+
map(
31+
select(.filename | match("\\.odin$")) |
32+
.filename |
33+
match("exercises/practice/([^/]+)/") |
34+
.captures[0].string
35+
) | unique[]
36+
' | xargs -r -L1 bin/test-one

.github/workflows/test.yml

Lines changed: 16 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
1-
# This workflow will verify the exercises in the repository.
2-
#
3-
# Requires scripts:
4-
# - bin/verify-exercises
5-
61
name: Test
72

83
on:
94
push:
10-
branches: [main]
11-
pull_request:
12-
workflow_dispatch:
5+
branches:
6+
- main
137

148
jobs:
15-
verify_exercises:
16-
# TODO: replace with another image if required to run the tests (optional)
9+
test:
1710
runs-on: ubuntu-24.04
1811

1912
steps:
20-
- name: Checkout repository
21-
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
13+
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
14+
15+
- uses: leafo/gh-actions-lua@8c9e175e7a3d77e21f809eefbee34a19b858641b
16+
with:
17+
luaVersion: 5.4
2218

23-
# TODO: setup any tooling that is required to run the tests (optional)
24-
# E.g. install a specific version of a programming language
25-
# E.g. install packages via apt/apk/yum/etc.
26-
# Find GitHub Actions to setup tooling here:
27-
# - https://github.com/actions/?q=setup&type=&language=
28-
# - https://github.com/actions/starter-workflows/tree/main/ci
29-
# - https://github.com/marketplace?type=actions&query=setup
30-
# - name: Use <setup tooling>
31-
# uses: <action to setup tooling>
19+
- uses: leafo/gh-actions-luarocks@97053c556d6ce2c8e26eb7ac93743437c7af7248
3220

33-
# TODO: install any dependencies (optional)
34-
# E.g. npm install, bundle install, etc.
35-
# - name: Install project dependencies
36-
# run: <install dependencies>
21+
- name: build
22+
run: |
23+
luarocks install busted
24+
luarocks install moonscript
3725
38-
- name: Verify all exercises
39-
run: bin/verify-exercises
26+
- name: test
27+
run: |
28+
bin/test-all

bin/add-practice-exercise

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ help_and_exit() {
2020
echo >&2 "Scaffold the files for a new practice exercise."
2121
echo >&2 "Usage: ${scriptname} [-h] [-a author] [-d difficulty] <exercise-slug>"
2222
echo >&2 "Where: author is the GitHub username of the exercise creator."
23-
echo >&2 "Where: difficulty is between 1 (easiest) to 10 (hardest)."
23+
echo >&2 " : difficulty is between 1 (easiest) to 10 (hardest)."
2424
exit 1
2525
}
2626

@@ -37,6 +37,7 @@ require_files_template() {
3737
}
3838

3939
required_tool jq
40+
required_tool curl
4041

4142
require_files_template "solution"
4243
require_files_template "test"
@@ -45,7 +46,7 @@ require_files_template "example"
4546
[[ -f ./bin/fetch-configlet ]] || die "Run this script from the repo's root directory."
4647

4748
author=''
48-
difficulty='1'
49+
difficulty='4'
4950
while getopts :ha:d: opt; do
5051
case $opt in
5152
h) help_and_exit ;;
@@ -59,6 +60,7 @@ shift "$((OPTIND - 1))"
5960
(( $# >= 1 )) || help_and_exit
6061

6162
slug="${1}"
63+
snake_slug=${1//-/_}
6264

6365
if [[ -z "${author}" ]]; then
6466
read -rp 'Your GitHub username: ' author
@@ -67,18 +69,71 @@ fi
6769
./bin/fetch-configlet
6870
./bin/configlet create --practice-exercise "${slug}" --author "${author}" --difficulty "${difficulty}"
6971

72+
filter='.exercises.practice |= sort_by(.difficulty, (.name|ascii_upcase))'
73+
jq "${filter}" config.json > config.sorted && mv config.sorted config.json
74+
7075
exercise_dir="exercises/practice/${slug}"
71-
files=$(jq -r --arg dir "${exercise_dir}" '.files | to_entries | map({key: .key, value: (.value | map("'"'"'" + $dir + "/" + . + "'"'"'") | join(" and "))}) | from_entries' "${exercise_dir}/.meta/config.json")
76+
files=$(
77+
jq -r --arg dir "${exercise_dir}" --arg q "'" '
78+
.files
79+
| to_entries
80+
| map({key: .key, value: (.value | map($q + $dir + "/" + . + $q) | join(" and "))})
81+
| from_entries
82+
' "${exercise_dir}/.meta/config.json"
83+
)
84+
85+
cp exercises/practice/hello-world/.busted "exercises/practice/${slug}/.busted"
86+
87+
mkdir -p canonical-data
88+
github="https://raw.githubusercontent.com/exercism/problem-specifications/refs/heads/main/exercises/${slug}/canonical-data.json"
89+
90+
curl -s -o "canonical-data/${slug}.json" "$github"
91+
92+
93+
camel_slug=$(perl -pe 's/(?:^|-)([a-z])/\u$1/g' <<< "$slug")
94+
cat << SPEC_GENERATOR > exercises/practice/${slug}/.meta/spec_generator.moon
95+
{
96+
module_name: '${camel_slug}',
97+
-- or, module_imports: {'func1', 'func2', ...},
98+
99+
-- optional:
100+
test_helpers: [[
101+
A block of code here, indented 2 spaces
102+
]]
103+
104+
generate_test: (case, level) ->
105+
local lines
106+
-- you may want to "switch case.property" here
107+
lines = {
108+
"result = ${camel_slug}.#{case.property} #{case.input}",
109+
"expected = #{quote case.expected}",
110+
"assert.are.equal expected, result"
111+
}
112+
table.concat [indent line, level for line in *lines], '\n'
113+
}
114+
SPEC_GENERATOR
115+
72116

73117
cat << NEXT_STEPS
74118
75119
Your next steps are:
76-
- Create the test suite in $(jq -r '.test' <<< "${files}")
77-
- The tests should be based on the canonical data at 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json'
78-
- Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false"
79-
- Create the example solution in $(jq -r '.example' <<< "${files}")
80-
- Verify the example solution passes the tests by running 'bin/verify-exercises ${slug}'
81-
- Create the stub solution in $(jq -r '.solution' <<< "${files}")
82-
- Update the 'difficulty' value for the exercise's entry in the 'config.json' file in the repo's root
83-
- Validate CI using 'bin/configlet lint' and 'bin/configlet fmt'
120+
121+
1. Create the test suite ==> $(jq -r '.test' <<< "${files}")
122+
123+
- A stub test generator awaits you --> 'exercises/practice/${slug}/.meta/spec_generator.moon'
124+
- If you don't want it, delete it.
125+
- Otherwise, populate it and run it --> $ bin/generate-spec ${slug}
126+
127+
- The tests should be based on the canonical data --> ./canonical-data/${slug}.json
128+
- Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false"
129+
130+
2. Create the example solution ==> $(jq -r '.example' <<< "${files}")
131+
132+
3. Verify the example solution passes the tests ==> $ bin/test-one ${slug}
133+
134+
4. Create the stub solution ==> $(jq -r '.solution' <<< "${files}")
135+
136+
5. Update the 'difficulty' value for the exercise's entry in the 'config.json' file in the repo's root
137+
138+
6. Validate CI using 'bin/configlet lint' and 'bin/configlet fmt'
84139
NEXT_STEPS

bin/generate-spec

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
#!/usr/bin/env moon
2+
-- ref: https://github.com/exercism/lua/blob/main/bin/generate-spec
3+
4+
require 'moonscript'
5+
json = (require 'dkjson').use_lpeg!
6+
-- import p from require 'moon'
7+
8+
local exercise_name, exercise_directory, spec_generator, included_tests -- forward declarations
9+
10+
11+
file_exists = (path) ->
12+
f = io.open path, 'r'
13+
if f
14+
f\close!
15+
true
16+
else
17+
false
18+
19+
20+
read_file = (path) ->
21+
f = assert io.open path, 'r'
22+
contents = f\read '*a'
23+
f\close!
24+
contents
25+
26+
27+
write_file = (path, contents) ->
28+
f = assert io.open path, 'w'
29+
f\write contents
30+
f\close!
31+
32+
33+
included_tests_from_toml = (path) ->
34+
included = {}
35+
local uuid, last_uuid
36+
line_no = 0
37+
38+
for line in io.lines(path)
39+
line_no += 1
40+
for uuid in line\gmatch('%[([%x%-]+)%]')
41+
last_uuid = uuid
42+
included[uuid] = true
43+
44+
if line\match('^include%s*=%s*false')
45+
included[last_uuid] = nil
46+
47+
included
48+
49+
50+
export indent, quote -- mark as global so spec_generators can see them
51+
52+
indent = (text, level) -> string.rep(' ', level) .. text
53+
54+
quote = (str) ->
55+
if str\find "'"
56+
"\"#{str\gsub '"', '\\"'}\""
57+
else
58+
"'#{str}'"
59+
60+
61+
test_cmd = 'it'
62+
63+
process = (node, level=0) ->
64+
if node.cases
65+
output = {}
66+
67+
if node.description
68+
table.insert output, indent("describe #{quote node.description}, ->", level)
69+
else
70+
table.insert output, indent("describe '#{exercise_name}', ->", level)
71+
72+
if spec_generator.test_helpers
73+
table.insert output, spec_generator.test_helpers
74+
75+
cases = {}
76+
for case in *node.cases
77+
if not case.uuid or included_tests[case.uuid]
78+
table.insert cases, process(case, level + 1)
79+
80+
table.insert output, table.concat(cases, '\n')
81+
82+
return table.concat output, '\n'
83+
84+
else -- no "cases" member
85+
test = "#{test_cmd} #{quote node.description}, ->\n#{spec_generator.generate_test(node, level + 1)}\n"
86+
test_cmd = 'pending'
87+
return indent test, level
88+
89+
-- ----------------------------------------------------------
90+
-- "main"
91+
-- ----------------------------------------------------------
92+
exercise_name = arg[1]
93+
snake_name = exercise_name\gsub("-", "_")
94+
95+
-- to differentiate from the lua rock "say" required by busted.
96+
if snake_name == 'say'
97+
snake_name = './say'
98+
99+
exercise_directory = 'exercises/practice/' .. exercise_name
100+
101+
canonical_data_url = "https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/#{exercise_name}/canonical-data.json"
102+
canonical_data_path = "canonical-data/#{exercise_name}.json"
103+
104+
assert os.execute('mkdir -p "$(dirname "' .. canonical_data_path .. '")"')
105+
assert os.execute('curl "' .. canonical_data_url .. '" -s -o "' .. canonical_data_path .. '"')
106+
107+
canonical_data = json.decode read_file canonical_data_path
108+
109+
tests_toml_path = exercise_directory .. '/.meta/tests.toml'
110+
included_tests = included_tests_from_toml tests_toml_path
111+
112+
package.moonpath = "#{exercise_directory}/.meta/?.moon;#{package.moonpath}"
113+
spec_generator = require 'spec_generator'
114+
115+
local spec
116+
if spec_generator.module_name
117+
spec = "#{spec_generator.module_name} = require '#{snake_name}'"
118+
elseif spec_generator.module_imports
119+
spec = "import #{table.concat spec_generator.module_imports, ', '} from require '#{snake_name}'"
120+
else
121+
error 'spec_generator is missing both "module_name" and "module_imports"'
122+
123+
spec ..= "\n\n" .. process(canonical_data)
124+
125+
spec_path = exercise_directory .. '/' .. snake_name .. '_spec.moon'
126+
write_file spec_path, spec
127+
128+
print "Created #{spec_path}"

bin/test-all

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
3+
start=$SECONDS
4+
5+
unset CDPATH
6+
7+
script_dir=$(realpath "$(dirname "$0")")
8+
9+
cd "${script_dir}/../exercises/practice" || exit 4
10+
result=0
11+
count=0
12+
13+
for exercise in *; do
14+
[[ -d $exercise ]] || continue
15+
16+
"$script_dir"/test-one "$exercise" || (( ++result ))
17+
(( ++count ))
18+
done
19+
20+
echo
21+
echo "$count exercises tested in $(( SECONDS - start )) seconds."
22+
23+
exit $result

0 commit comments

Comments
 (0)