Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/pr.yml.not_active
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: PR

on:
pull_request:

jobs:
test:
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3

- uses: leafo/gh-actions-lua@8c9e175e7a3d77e21f809eefbee34a19b858641b
with:
luaVersion: 5.4

- uses: leafo/gh-actions-luarocks@97053c556d6ce2c8e26eb7ac93743437c7af7248

- name: build
run: |
luarocks install busted
luarocks install moonscript

- name: Run tests for changed/added exercises
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
pr_endpoint=$(jq -r '"repos/\(.repository.full_name)/pulls/\(.pull_request.number)"' "$GITHUB_EVENT_PATH")
gh api "$pr_endpoint/files" --paginate --jq '
map(
select(.filename | match("\\.odin$")) |
.filename |
match("exercises/practice/([^/]+)/") |
.captures[0].string
) | unique[]
' | xargs -r -L1 bin/test-one
43 changes: 16 additions & 27 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,28 @@
# This workflow will verify the exercises in the repository.
#
# Requires scripts:
# - bin/verify-exercises

name: Test

on:
push:
branches: [main]
pull_request:
workflow_dispatch:
branches:
- main

jobs:
verify_exercises:
# TODO: replace with another image if required to run the tests (optional)
test:
runs-on: ubuntu-24.04

steps:
- name: Checkout repository
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3

- uses: leafo/gh-actions-lua@8c9e175e7a3d77e21f809eefbee34a19b858641b
with:
luaVersion: 5.4

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

# TODO: install any dependencies (optional)
# E.g. npm install, bundle install, etc.
# - name: Install project dependencies
# run: <install dependencies>
- name: build
run: |
luarocks install busted
luarocks install moonscript

- name: Verify all exercises
run: bin/verify-exercises
- name: test
run: |
bin/test-all
77 changes: 66 additions & 11 deletions bin/add-practice-exercise
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ help_and_exit() {
echo >&2 "Scaffold the files for a new practice exercise."
echo >&2 "Usage: ${scriptname} [-h] [-a author] [-d difficulty] <exercise-slug>"
echo >&2 "Where: author is the GitHub username of the exercise creator."
echo >&2 "Where: difficulty is between 1 (easiest) to 10 (hardest)."
echo >&2 " : difficulty is between 1 (easiest) to 10 (hardest)."
exit 1
}

Expand All @@ -37,6 +37,7 @@ require_files_template() {
}

required_tool jq
required_tool curl

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

author=''
difficulty='1'
difficulty='4'
while getopts :ha:d: opt; do
case $opt in
h) help_and_exit ;;
Expand All @@ -59,6 +60,7 @@ shift "$((OPTIND - 1))"
(( $# >= 1 )) || help_and_exit

slug="${1}"
snake_slug=${1//-/_}

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

filter='.exercises.practice |= sort_by(.difficulty, (.name|ascii_upcase))'
jq "${filter}" config.json > config.sorted && mv config.sorted config.json

exercise_dir="exercises/practice/${slug}"
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")
files=$(
jq -r --arg dir "${exercise_dir}" --arg q "'" '
.files
| to_entries
| map({key: .key, value: (.value | map($q + $dir + "/" + . + $q) | join(" and "))})
| from_entries
' "${exercise_dir}/.meta/config.json"
)

cp exercises/practice/hello-world/.busted "exercises/practice/${slug}/.busted"

mkdir -p canonical-data
github="https://raw.githubusercontent.com/exercism/problem-specifications/refs/heads/main/exercises/${slug}/canonical-data.json"

curl -s -o "canonical-data/${slug}.json" "$github"


camel_slug=$(perl -pe 's/(?:^|-)([a-z])/\u$1/g' <<< "$slug")
cat << SPEC_GENERATOR > exercises/practice/${slug}/.meta/spec_generator.moon
{
module_name: '${camel_slug}',
-- or, module_imports: {'func1', 'func2', ...},

-- optional:
test_helpers: [[
A block of code here, indented 2 spaces
]]

generate_test: (case, level) ->
local lines
-- you may want to "switch case.property" here
lines = {
"result = ${camel_slug}.#{case.property} #{case.input}",
"expected = #{quote case.expected}",
"assert.are.equal expected, result"
}
table.concat [indent line, level for line in *lines], '\n'
}
SPEC_GENERATOR


cat << NEXT_STEPS

Your next steps are:
- Create the test suite in $(jq -r '.test' <<< "${files}")
- The tests should be based on the canonical data at 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json'
- Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false"
- Create the example solution in $(jq -r '.example' <<< "${files}")
- Verify the example solution passes the tests by running 'bin/verify-exercises ${slug}'
- Create the stub solution in $(jq -r '.solution' <<< "${files}")
- Update the 'difficulty' value for the exercise's entry in the 'config.json' file in the repo's root
- Validate CI using 'bin/configlet lint' and 'bin/configlet fmt'

1. Create the test suite ==> $(jq -r '.test' <<< "${files}")

- A stub test generator awaits you --> 'exercises/practice/${slug}/.meta/spec_generator.moon'
- If you don't want it, delete it.
- Otherwise, populate it and run it --> $ bin/generate-spec ${slug}

- The tests should be based on the canonical data --> ./canonical-data/${slug}.json
- Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false"

2. Create the example solution ==> $(jq -r '.example' <<< "${files}")

3. Verify the example solution passes the tests ==> $ bin/test-one ${slug}

4. Create the stub solution ==> $(jq -r '.solution' <<< "${files}")

5. Update the 'difficulty' value for the exercise's entry in the 'config.json' file in the repo's root

6. Validate CI using 'bin/configlet lint' and 'bin/configlet fmt'
NEXT_STEPS
128 changes: 128 additions & 0 deletions bin/generate-spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env moon
-- ref: https://github.com/exercism/lua/blob/main/bin/generate-spec

require 'moonscript'
json = (require 'dkjson').use_lpeg!
-- import p from require 'moon'

local exercise_name, exercise_directory, spec_generator, included_tests -- forward declarations


file_exists = (path) ->
f = io.open path, 'r'
if f
f\close!
true
else
false


read_file = (path) ->
f = assert io.open path, 'r'
contents = f\read '*a'
f\close!
contents


write_file = (path, contents) ->
f = assert io.open path, 'w'
f\write contents
f\close!


included_tests_from_toml = (path) ->
included = {}
local uuid, last_uuid
line_no = 0

for line in io.lines(path)
line_no += 1
for uuid in line\gmatch('%[([%x%-]+)%]')
last_uuid = uuid
included[uuid] = true

if line\match('^include%s*=%s*false')
included[last_uuid] = nil

included


export indent, quote -- mark as global so spec_generators can see them

indent = (text, level) -> string.rep(' ', level) .. text

quote = (str) ->
if str\find "'"
"\"#{str\gsub '"', '\\"'}\""
else
"'#{str}'"


test_cmd = 'it'

process = (node, level=0) ->
if node.cases
output = {}

if node.description
table.insert output, indent("describe #{quote node.description}, ->", level)
else
table.insert output, indent("describe '#{exercise_name}', ->", level)

if spec_generator.test_helpers
table.insert output, spec_generator.test_helpers

cases = {}
for case in *node.cases
if not case.uuid or included_tests[case.uuid]
table.insert cases, process(case, level + 1)

table.insert output, table.concat(cases, '\n')

return table.concat output, '\n'

else -- no "cases" member
test = "#{test_cmd} #{quote node.description}, ->\n#{spec_generator.generate_test(node, level + 1)}\n"
test_cmd = 'pending'
return indent test, level

-- ----------------------------------------------------------
-- "main"
-- ----------------------------------------------------------
exercise_name = arg[1]
snake_name = exercise_name\gsub("-", "_")

-- to differentiate from the lua rock "say" required by busted.
if snake_name == 'say'
snake_name = './say'

exercise_directory = 'exercises/practice/' .. exercise_name

canonical_data_url = "https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/#{exercise_name}/canonical-data.json"
canonical_data_path = "canonical-data/#{exercise_name}.json"

assert os.execute('mkdir -p "$(dirname "' .. canonical_data_path .. '")"')
assert os.execute('curl "' .. canonical_data_url .. '" -s -o "' .. canonical_data_path .. '"')

canonical_data = json.decode read_file canonical_data_path

tests_toml_path = exercise_directory .. '/.meta/tests.toml'
included_tests = included_tests_from_toml tests_toml_path

package.moonpath = "#{exercise_directory}/.meta/?.moon;#{package.moonpath}"
spec_generator = require 'spec_generator'

local spec
if spec_generator.module_name
spec = "#{spec_generator.module_name} = require '#{snake_name}'"
elseif spec_generator.module_imports
spec = "import #{table.concat spec_generator.module_imports, ', '} from require '#{snake_name}'"
else
error 'spec_generator is missing both "module_name" and "module_imports"'

spec ..= "\n\n" .. process(canonical_data)

spec_path = exercise_directory .. '/' .. snake_name .. '_spec.moon'
write_file spec_path, spec

print "Created #{spec_path}"
23 changes: 23 additions & 0 deletions bin/test-all
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#!/usr/bin/env bash

start=$SECONDS

unset CDPATH

script_dir=$(realpath "$(dirname "$0")")

cd "${script_dir}/../exercises/practice" || exit 4
result=0
count=0

for exercise in *; do
[[ -d $exercise ]] || continue

"$script_dir"/test-one "$exercise" || (( ++result ))
(( ++count ))
done

echo
echo "$count exercises tested in $(( SECONDS - start )) seconds."

exit $result
Loading