diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..d5df3db --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,18 @@ +name: Track Exercises on Runner + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +jobs: + ci: + runs-on: ubuntu-24.04-arm + + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + - run: docker pull exercism/factor-test-runner + - name: Run tests for all exercises + run: sh ./bin/test diff --git a/bin/.test-in-docker b/bin/.test-in-docker new file mode 100644 index 0000000..f0d869a --- /dev/null +++ b/bin/.test-in-docker @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +# This script is meant to be run in the docker container of factor-test-runner + +all_slugs="$*" + +exit_code=0 + +for slug in $all_slugs; do + local_exit_code=0 + + rm -rf /tmp/solution + cp -r "/exercises/practice/${slug}" /tmp/solution + + # Integration test: Check if the example solution passes the tests + # as run by the factor-test-runner similiarly to a student submitting + # their solution. + cp /tmp/solution/.meta/example.factor "/tmp/solution/${slug}/${slug}.factor" + bin/run.sh "${slug}" /tmp/solution /tmp/solution > /dev/null + solution_pattern=$(jq -r tostring /tmp/solution/results.json | grep "{\"version\":1,\"status\":\"pass\"") + + errors="" + + if [ -z "$solution_pattern" ]; then + errors="${errors}\n\nSolution is incorrect:\n$(jq -r '.message' /tmp/solution/results.json)" + local_exit_code=1; + fi + + if [ $local_exit_code = 0 ]; then + echo -e "${slug}: \e[32mPASSED\e[0m" + else + exit_code=1 + echo -e "${slug}: \e[31mFAILED\e[0m\n${errors}\n\n" + fi +done + +exit ${exit_code} diff --git a/bin/test b/bin/test new file mode 100755 index 0000000..776e008 --- /dev/null +++ b/bin/test @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +# Synopsis: +# Test the factor track's exercises. +# +# For each exercise we test if the example solution +# .meta/example.factor passes the tests. +# +# Usage: +# ./bin/test [slug...] + +all_slugs="$*" +if [ -z "$all_slugs" ]; then + all_slugs=$(ls ./exercises/practice/) +fi + +docker run --rm --entrypoint bash \ + --network none \ + --mount type=bind,src="$(realpath ./exercises)",dst=/exercises,ro \ + --mount type=bind,src="$(realpath ./bin)",dst=/scripts,ro \ + --mount type=tmpfs,dst=/tmp \ + exercism/factor-test-runner \ + /scripts/.test-in-docker "$all_slugs" + +exit_code=$? + +exit ${exit_code} diff --git a/bin/verify-exercises-in-docker b/bin/verify-exercises-in-docker new file mode 100755 index 0000000..b0e516c --- /dev/null +++ b/bin/verify-exercises-in-docker @@ -0,0 +1,112 @@ +#!/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 + +# Example: verify all exercises against specified test runner +# bin/verify-exercises-in-docker -i my-local-image + +set -e +shopt -s nullglob + +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." +} + +copy_example_or_exemplar_to_solution() { + local dir="${1}" + jq -r '[.files.solution, .files.exemplar // .files.example] | transpose | map(select(.[0] and .[1]))[][]' "${dir}/.meta/config.json" \ + | while read -r dst; read -r src; do + cp "${dir}/${src}" "${dir}/${dst}" + done +} + +run_tests() { + local slug="${1}" dir="${2}" + local -a docker_args + + docker_args+=( --rm --network none ) + docker_args+=( --mount "type=bind,src=${dir},dst=/solution" ) + # /tmp needs to be a proper volume to run the compiled executable; tmpfs is not executable. + docker_args+=( --mount "type=volume,dst=/tmp" ) + + # /solution is used both as the location to read the code from and as a destination for the results.json file. + docker run "${docker_args[@]}" "${image}" "${slug}" /solution /solution + jq -e '.status == "pass"' "${dir}/results.json" >/dev/null 2>&1 +} + +verify_exercise() { + local dir slug tmpdir + dir="${1%/}" + slug="${dir##*/}" + tmpdir="$(mktemp -d -t "exercism-verify-${slug}-XXXXX")" + + if jq -e --arg slug "${slug}" '.exercises.practice[] | select(.slug == $slug and .status == "wip")' config.json >/dev/null 2>&1; then + echo "Skipping ${slug} (wip)..." + return 0 + fi + + echo "Verifying ${slug} exercise..." + ( + trap 'rm -rf "${tmpdir}"' EXIT # remove tempdir when subshell ends + cp -r "${dir}/." "${tmpdir}" || exit + copy_example_or_exemplar_to_solution "${tmpdir}" + run_tests "${slug}" "${tmpdir}" || { cat "${tmpdir}/results.json"; exit 1; } + ) +} + +verify_exercises() { + local -a exercises + local parent path + if (( $# )); then + for slug; do + for parent in concept practice; do + path="./exercises/${parent}/${slug}" + [[ -d "${path}" ]] && exercises+=( "${path}" ) + done + done + else + exercises=( ./exercises/{concept,practice}/* ) + fi + (( ${#exercises[@]} )) || die "No matching exercises found" + + rc=0 + for exercise_dir in "${exercises[@]}"; do + verify_exercise "${exercise_dir}" || rc=$? + done + return "$rc" +} + + +required_tool docker +required_tool jq + +image='' +while getopts i: opt; do + case "${opt}" in + i) image="${OPTARG}" ;; + ?) die "Unknown option: -$OPTARG" ;; + esac +done +shift "$((OPTIND - 1))" + +if [[ -z "${image}" ]]; then + image="exercism/factor-test-runner:latest" + # docker pull "${image}" || + # die "docker pull ${image} failed. Check the test runner docs at https://exercism.org/docs/building/tooling/test-runners for more information." +fi + +verify_exercises "$@" diff --git a/exercises/practice/accumulate/.meta/example.factor b/exercises/practice/accumulate/.meta/example.factor index 1a7483a..e23bc3d 100644 --- a/exercises/practice/accumulate/.meta/example.factor +++ b/exercises/practice/accumulate/.meta/example.factor @@ -1,13 +1,5 @@ -USING: arrays kernel locals sequences sequences.extras ; +USING: kernel locals make sequences ; IN: accumulate :: accum ( seq quot: ( x -- y ) -- newseq ) - seq >resizable :> seq - seq length seq new-resizable :> newseq - - [ seq empty? ] [ - seq pop quot call - newseq push - ] until - - newseq reverse >array ; inline + [ seq [ quot call , ] each ] { } make ; inline