diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70cce63..8fe86be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,4 +40,4 @@ jobs: build-args: ZIP_PASSWORD=${{secrets.ZIP_PASSWORD}} - name: Run tests for all exercises - run: bin/test + run: bin/verify-exercises-in-docker diff --git a/bin/test b/bin/test deleted file mode 100755 index 27fa4e2..0000000 --- a/bin/test +++ /dev/null @@ -1,66 +0,0 @@ -#!/usr/bin/env bash - -# Test if the example/exemplar solution of each -# Practice/Concept Exercise passes the exercise's tests. - -# Example: -# ./bin/test - -set -eo pipefail - -exit_code=0 - -function run_test_runner() { - local dir=$1 - local slug=$2 - - docker run \ - --rm \ - --network none \ - --mount type=bind,src="${dir}",dst=/solution \ - --mount type=bind,src="${dir}",dst=/output \ - --tmpfs /tmp:rw \ - exercism/8th-test-runner "${slug}" "/solution" "/output" -} - -function verify_exercise() { - local dir=$(realpath "$1") - local slug=$(basename "${dir}") - local implementation_file_key=$2 - local implementation_file=$(jq -r --arg d "${dir}" --arg k "${implementation_file_key}" '$d + "/" + .files[$k][0]' "${dir}/.meta/config.json") - local stub_file=$(jq -r --arg d "${dir}" '$d + "/" + .files.solution[0]' "${dir}/.meta/config.json") - local stub_backup_file="${stub_file}.bak" - local results_file="${dir}/results.json" - - cp "${stub_file}" "${stub_backup_file}" - cp "${implementation_file}" "${stub_file}" - - run_test_runner "${dir}" "${slug}" - - if [[ $(jq -r '.status' "${results_file}") != "pass" ]]; then - echo "${slug}: ${implementation_file_key} solution did not pass the tests" - cat "${results_file}" - exit_code=1 - fi - - mv "${stub_backup_file}" "${stub_file}" - rm -f "${results_file}" -} - -# Verify the Concept Exercises -for concept_exercise_dir in ./exercises/concept/*/; do - if [[ -d $concept_exercise_dir ]]; then - echo "Checking $(basename "${concept_exercise_dir}") exercise..." - verify_exercise "$concept_exercise_dir" "exemplar" - fi -done - -# Verify the Practice Exercises -for practice_exercise_dir in ./exercises/practice/*/; do - if [[ -d $practice_exercise_dir ]]; then - echo "Checking $(basename "${practice_exercise_dir}") exercise..." - verify_exercise "$practice_exercise_dir" "example" - fi -done - -exit "${exit_code}" diff --git a/bin/test-no-docker b/bin/test-no-docker deleted file mode 100755 index 9dac856..0000000 --- a/bin/test-no-docker +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash - -usage="Usage: $0 [-r] -a|exercise_slug - -Test an example solution. -Use -a to test all exercises. -Use -r to run skipped tests. - -Example: $0 luhn" - -die() { echo "$*" >&2; exit 1; } - -make_test_dir() { - local exercise_dir=$1 - local slug=${exercise_dir##*/} - local key=$2 - ( - cd "$exercise_dir" || die "cannot cd to: $exercise_dir" - [[ -f test-words.8th ]] && cp test-words.8th "$test_dir" - [[ -d libs ]] && cp -r libs "$test_dir" - while IFS= read -r test; do - cp -r "$test" "$test_dir" - done < <( - jq -r '.files.test[]' .meta/config.json - ) - while IFS= read -r solution; do - cp -r "$solution" "$test_dir/$slug.8th" - done < <( - jq -r --arg key "$key" '.files[$key][]' .meta/config.json - ) - ) -} - -test_one() { - local slug=$1 - if [[ -d "./exercises/concept/$slug" ]]; then - make_test_dir "./exercises/concept/$slug" exemplar - elif [[ -d "./exercises/practice/$slug" ]]; then - make_test_dir "./exercises/practice/$slug" example - else - die "no such exercise: $slug" - fi - ( - cd "$test_dir" || die "cannot cd to $test_dir" - 8th -f test.8th - ) -} - -cleanup() { rm -rf "$test_dir"; } - -test_dir=$(mktemp -d) -trap cleanup EXIT - -all=false -while getopts :har opt; do - case $opt in - h) die "$usage" ;; - a) all=true ;; - r) export RUN_ALL_TESTS=true ;; - ?) die "unknown option: -$OPTARG" ;; - esac -done -shift $((OPTIND-1)) - -if $all; then - shopt -s nullglob - for dir in ./exercises/concept/* ./exercises/practice/*; do - slug=${dir##*/} - if test_one "$slug"; then - echo "$slug tests exited with $?" - echo - else - die "$slug tests exited with $?" - fi - done -else - slug=$1 - [[ -n $slug ]] || die "$usage" - test_one "$slug" -fi diff --git a/bin/verify-exercises b/bin/verify-exercises new file mode 100755 index 0000000..f5e9e11 --- /dev/null +++ b/bin/verify-exercises @@ -0,0 +1,81 @@ +#!/usr/bin/env bash + +# Synopsis: +# Verify that each exercise's example/exemplar solution passes the tests. +# You can either verify all exercises or a single exercise. + +# Example: verify all exercises +# bin/verify-exercises + +# Example: verify single exercise +# bin/verify-exercises two-fer + +set -eo pipefail + +die() { echo "$*" >&2; exit 1; } + +required_tool() { + command -v "${1}" >/dev/null 2>&1 || + die "${1} is required but not installed. Please install it and make sure it's in your PATH." +} + +required_tool jq + +copy_example_or_examplar_to_solution() { + jq -c '[.files.solution, .files.exemplar // .files.example] | transpose | map({src: .[1], dst: .[0]}) | .[]' .meta/config.json \ + | while read -r src_and_dst; do + cp "$(jq -r '.src' <<< "${src_and_dst}")" "$(jq -r '.dst' <<< "${src_and_dst}")" + done +} + +unskip_tests() { + # shellcheck disable=SC2034 + jq -r '.files.test[]' .meta/config.json | while read -r test_file; do + sed -i 's/SKIP-REST-OF-TESTS//g' "${test_file}" + done +} + +run_tests() { + 8th -f test.8th +} + +verify_exercise() { + local dir + local slug + local tmp_dir + + dir=$(realpath "${1}") + slug=$(basename "${dir}") + tmp_dir=$(mktemp -d -t "exercism-verify-${slug}-XXXXX") + + echo "Verifying ${slug} exercise..." + + ( + trap 'rm -rf "$tmp_dir"' EXIT # remove tempdir when subshell ends + cp -r "${dir}/." "${tmp_dir}" + cd "${tmp_dir}" + + copy_example_or_examplar_to_solution + unskip_tests + run_tests + ) +} + +verify_exercises() { + local exercise_slug + + exercise_slug="${1}" + + shopt -s nullglob + count=0 + for exercise_dir in ./exercises/{concept,practice}/${exercise_slug}/; do + if [[ -d "${exercise_dir}" ]]; then + verify_exercise "${exercise_dir}" + ((++count)) + fi + done + ((count > 0)) || die 'no matching exercises found!' +} + +exercise_slug="${1:-*}" +verify_exercises "${exercise_slug}" diff --git a/bin/verify-exercises-in-docker b/bin/verify-exercises-in-docker new file mode 100755 index 0000000..0ad47c0 --- /dev/null +++ b/bin/verify-exercises-in-docker @@ -0,0 +1,84 @@ +#!/usr/bin/env bash + +# Synopsis: +# Verify that each exercise's example/exemplar solution passes the tests +# using the track's test runner Docker image. +# You can either verify all exercises or a single exercise. + +# Example: verify all exercises in Docker +# bin/verify-exercises-in-docker + +# Example: verify single exercise in Docker +# bin/verify-exercises-in-docker two-fer + +set -eo pipefail + +die() { echo "$*" >&2; exit 1; } + +required_tool() { + command -v "${1}" >/dev/null 2>&1 || + die "${1} is required but not installed. Please install it and make sure it's in your PATH." +} + +required_tool docker + +copy_example_or_examplar_to_solution() { + jq -c '[.files.solution, .files.exemplar // .files.example] | transpose | map({src: .[1], dst: .[0]}) | .[]' .meta/config.json \ + | while read -r src_and_dst; do + cp "$(jq -r '.src' <<< "${src_and_dst}")" "$(jq -r '.dst' <<< "${src_and_dst}")" + done +} + +run_tests() { + local slug + slug="${1}" + + # shellcheck disable=SC1083 + docker run \ + --rm \ + --network none \ + --read-only \ + --mount type=bind,src="${PWD}",dst=/solution \ + --mount type=bind,src="${PWD}",dst=/output \ + --mount type=tmpfs,dst=/tmp \ + exercism/8th-test-runner "${slug}" /solution /output + jq -e '.status == "pass"' "${PWD}/results.json" >/dev/null 2>&1 +} + +verify_exercise() { + local dir + local slug + local tmp_dir + dir=$(realpath "${1}") + slug=$(basename "${dir}") + tmp_dir=$(mktemp -d -t "exercism-verify-${slug}-XXXXX") + + echo "Verifying ${slug} exercise..." + + ( + trap 'rm -rf "$tmp_dir"' EXIT # remove tempdir when subshell ends + cp -r "${dir}/." "${tmp_dir}" + cd "${tmp_dir}" + + copy_example_or_examplar_to_solution + run_tests "${slug}" + ) +} + +verify_exercises() { + local exercise_slug + exercise_slug="${1}" + + shopt -s nullglob + count=0 + for exercise_dir in ./exercises/{concept,practice}/${exercise_slug}/; do + if [[ -d "${exercise_dir}" ]]; then + verify_exercise "${exercise_dir}" + ((++count)) + fi + done + ((count > 0)) || die 'no matching exercises found!' +} + +exercise_slug="${1:-*}" +verify_exercises "${exercise_slug}"