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
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions bin/.test-in-docker
Original file line number Diff line number Diff line change
@@ -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}
27 changes: 27 additions & 0 deletions bin/test
Original file line number Diff line number Diff line change
@@ -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}
112 changes: 112 additions & 0 deletions bin/verify-exercises-in-docker
Original file line number Diff line number Diff line change
@@ -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 "$@"
12 changes: 2 additions & 10 deletions exercises/practice/accumulate/.meta/example.factor
Original file line number Diff line number Diff line change
@@ -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