diff --git a/.github/workflows/pr.yml.not_active b/.github/workflows/pr.yml.not_active new file mode 100644 index 0000000..591664c --- /dev/null +++ b/.github/workflows/pr.yml.not_active @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1052627..874a5df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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 - # uses: + - uses: leafo/gh-actions-luarocks@97053c556d6ce2c8e26eb7ac93743437c7af7248 - # TODO: install any dependencies (optional) - # E.g. npm install, bundle install, etc. - # - name: Install project dependencies - # run: + - name: build + run: | + luarocks install busted + luarocks install moonscript - - name: Verify all exercises - run: bin/verify-exercises + - name: test + run: | + bin/test-all diff --git a/bin/add-practice-exercise b/bin/add-practice-exercise index 7c86306..d097a86 100755 --- a/bin/add-practice-exercise +++ b/bin/add-practice-exercise @@ -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] " 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 } @@ -37,6 +37,7 @@ require_files_template() { } required_tool jq +required_tool curl require_files_template "solution" require_files_template "test" @@ -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 ;; @@ -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 @@ -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 diff --git a/bin/generate-spec b/bin/generate-spec new file mode 100755 index 0000000..eff7614 --- /dev/null +++ b/bin/generate-spec @@ -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}" diff --git a/bin/test-all b/bin/test-all new file mode 100755 index 0000000..f31d8c8 --- /dev/null +++ b/bin/test-all @@ -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 diff --git a/bin/test-one b/bin/test-one new file mode 100755 index 0000000..e8ec430 --- /dev/null +++ b/bin/test-one @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# +# shellcheck disable=SC2164 + +if [[ -z $1 ]]; then + echo "Usage: $0 exercise-slug" >&2 + exit 2 +fi + +unset CDPATH + +cd "$(realpath "$(dirname "$0")/..")" + +exercise=$1 +shift +exercise_dir=$(realpath "exercises/practice/$exercise") + +if [[ ! -d $exercise_dir ]]; then + echo "No such exercise: $exercise" >&2 + exit 3 +fi + +test_dir=$( mktemp -d ) +trap 'rm -rf "$test_dir"' EXIT + +cd "$test_dir" || exit 4 +(cd "$exercise_dir"; tar cf - .) | tar xf - + +echo >&2 +echo "Testing $exercise..." >&2 + +IFS=$'\t' read -r solution tests example < <( + jq -r '.files | [.solution[0], .test[0], .example[0]] | @tsv' .meta/config.json +) + +mv "$example" "$solution" +perl -i -pe 's/^\s+\Kpending\b/it/' "$tests" + +busted --verbose "$@" diff --git a/config.json b/config.json index 145c83f..bed5c08 100644 --- a/config.json +++ b/config.json @@ -8,23 +8,53 @@ "representer": false, "analyzer": false }, - "blurb": "TODO: add blurb", + "blurb": "MoonScript is a dynamic scripting language that compiles into Lua. It gives you the power of one of the fastest scripting languages combined with a rich set of features.", "version": 3, "online_editor": { "indent_style": "space", - "indent_size": 4 + "indent_size": 2, + "highlightjs_language": "moonscript" }, "files": { - "solution": [], - "test": [], - "example": [], - "exemplar": [] + "solution": [ + "%{snake_slug}.moon" + ], + "test": [ + "%{snake_slug}_spec.moon" + ], + "example": [ + ".meta/example.moon" + ], + "exemplar": [ + ".meta/exemplar.moon" + ] }, "exercises": { - "concept": [], - "practice": [] + "practice": [ + { + "slug": "hello-world", + "name": "Hello World", + "uuid": "09b9866a-3f90-461b-8baf-70d9f0c02322", + "practices": [], + "prerequisites": [], + "difficulty": 1 + } + ] }, - "concepts": [], - "key_features": [], - "tags": [] + "tags": [ + "execution_mode/interpreted", + "paradigm/imperative", + "paradigm/object_oriented", + "paradigm/procedural", + "platform/linux", + "platform/mac", + "platform/windows", + "runtime/language_specific", + "typing/dynamic", + "used_for/backends", + "used_for/cross_platform_development", + "used_for/embedded_systems", + "used_for/games", + "used_for/scripts" + ] } diff --git a/exercises/practice/hello-world/.busted b/exercises/practice/hello-world/.busted new file mode 100644 index 0000000..86b84e7 --- /dev/null +++ b/exercises/practice/hello-world/.busted @@ -0,0 +1,5 @@ +return { + default = { + ROOT = { '.' } + } +} diff --git a/exercises/practice/hello-world/.docs/instructions.md b/exercises/practice/hello-world/.docs/instructions.md new file mode 100644 index 0000000..c9570e4 --- /dev/null +++ b/exercises/practice/hello-world/.docs/instructions.md @@ -0,0 +1,16 @@ +# Instructions + +The classical introductory exercise. +Just say "Hello, World!". + +["Hello, World!"][hello-world] is the traditional first program for beginning programming in a new language or environment. + +The objectives are simple: + +- Modify the provided code so that it produces the string "Hello, World!". +- Run the test suite and make sure that it succeeds. +- Submit your solution and check it at the website. + +If everything goes well, you will be ready to fetch your first real exercise. + +[hello-world]: https://en.wikipedia.org/wiki/%22Hello,_world!%22_program diff --git a/exercises/practice/hello-world/.meta/config.json b/exercises/practice/hello-world/.meta/config.json new file mode 100644 index 0000000..cfd6bb6 --- /dev/null +++ b/exercises/practice/hello-world/.meta/config.json @@ -0,0 +1,19 @@ +{ + "authors": [ + "glennj" + ], + "files": { + "solution": [ + "hello_world.moon" + ], + "test": [ + "hello_world_spec.moon" + ], + "example": [ + ".meta/example.moon" + ] + }, + "blurb": "Exercism's classic introductory exercise. Just say \"Hello, World!\".", + "source": "This is an exercise to introduce users to using Exercism", + "source_url": "https://en.wikipedia.org/wiki/%22Hello,_world!%22_program" +} diff --git a/exercises/practice/hello-world/.meta/example.moon b/exercises/practice/hello-world/.meta/example.moon new file mode 100644 index 0000000..e8adead --- /dev/null +++ b/exercises/practice/hello-world/.meta/example.moon @@ -0,0 +1,7 @@ +require "moonscript" + +hello = -> + "Hello, World!" + +{ :hello } + diff --git a/exercises/practice/hello-world/.meta/tests.toml b/exercises/practice/hello-world/.meta/tests.toml new file mode 100644 index 0000000..73466d6 --- /dev/null +++ b/exercises/practice/hello-world/.meta/tests.toml @@ -0,0 +1,13 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[af9ffe10-dc13-42d8-a742-e7bdafac449d] +description = "Say Hi!" diff --git a/exercises/practice/hello-world/hello_world.moon b/exercises/practice/hello-world/hello_world.moon new file mode 100644 index 0000000..1952403 --- /dev/null +++ b/exercises/practice/hello-world/hello_world.moon @@ -0,0 +1,6 @@ +require "moonscript" + +hello = -> + 'Goodbye, Mars!' + +{ :hello } diff --git a/exercises/practice/hello-world/hello_world_spec.moon b/exercises/practice/hello-world/hello_world_spec.moon new file mode 100644 index 0000000..92ba366 --- /dev/null +++ b/exercises/practice/hello-world/hello_world_spec.moon @@ -0,0 +1,10 @@ +-- Require the hello-world module +hello_world = require 'hello_world' + +-- Define a module named hello-world. This module should return a single +-- function named hello that takes no arguments and returns a string. + +describe 'hello-world', -> + it 'says hello world', -> + result = hello_world.hello! + assert.are.equal 'Hello, World!', result