From 67a4344f89d7008893f493082bd9e99a95fe20fa Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Mon, 13 Apr 2026 15:35:19 +1000 Subject: [PATCH 1/5] bin/verify-exercises-in-docker --- bin/verify-exercises-in-docker | 108 +++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100755 bin/verify-exercises-in-docker diff --git a/bin/verify-exercises-in-docker b/bin/verify-exercises-in-docker new file mode 100755 index 0000000..eec60d0 --- /dev/null +++ b/bin/verify-exercises-in-docker @@ -0,0 +1,108 @@ +#!/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_examplar_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="$(readlink -e "${1}")" + slug="${dir##*/}" + tmpdir="$(mktemp -d -t "exercism-verify-${slug}-XXXXX")" + + echo "Verifying ${slug} exercise..." + ( + trap 'rm -rf "${tmpdir}"' EXIT # remove tempdir when subshell ends + cp -r "${dir}/." "${tmpdir}" || exit + copy_example_or_examplar_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" +} + + +(( BASH_VERSINFO[0] >= 4 )) || die "Requires bash 4 or greater." +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/racket-test-runner" + 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 "$@" From 90e0ae84ff83d5175e71b0a33ba81d317f11be4b Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Tue, 14 Apr 2026 06:10:43 +1000 Subject: [PATCH 2/5] support bash 3 --- bin/verify-exercises-in-docker | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/verify-exercises-in-docker b/bin/verify-exercises-in-docker index eec60d0..7ee8aaf 100755 --- a/bin/verify-exercises-in-docker +++ b/bin/verify-exercises-in-docker @@ -50,7 +50,7 @@ run_tests() { verify_exercise() { local dir slug tmpdir - dir="$(readlink -e "${1}")" + dir="${1%/}" slug="${dir##*/}" tmpdir="$(mktemp -d -t "exercism-verify-${slug}-XXXXX")" @@ -86,7 +86,7 @@ verify_exercises() { } -(( BASH_VERSINFO[0] >= 4 )) || die "Requires bash 4 or greater." +(( BASH_VERSINFO[0] >= 3 )) || die "Requires bash 3 or greater." required_tool docker required_tool jq From 2c273edf7ed3917f0926169c9a2b311867316556 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Tue, 14 Apr 2026 06:31:29 +1000 Subject: [PATCH 3/5] omit bash version check --- bin/verify-exercises-in-docker | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/verify-exercises-in-docker b/bin/verify-exercises-in-docker index 7ee8aaf..9c4d4d6 100755 --- a/bin/verify-exercises-in-docker +++ b/bin/verify-exercises-in-docker @@ -86,7 +86,6 @@ verify_exercises() { } -(( BASH_VERSINFO[0] >= 3 )) || die "Requires bash 3 or greater." required_tool docker required_tool jq From 3cf977d721561a0efb8a14240cc67aff9e3c03c8 Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Tue, 14 Apr 2026 08:35:20 +1000 Subject: [PATCH 4/5] exemplar not examplar --- bin/verify-exercises-in-docker | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/verify-exercises-in-docker b/bin/verify-exercises-in-docker index 9c4d4d6..0068646 100755 --- a/bin/verify-exercises-in-docker +++ b/bin/verify-exercises-in-docker @@ -26,7 +26,7 @@ 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_examplar_to_solution() { +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 @@ -58,7 +58,7 @@ verify_exercise() { ( trap 'rm -rf "${tmpdir}"' EXIT # remove tempdir when subshell ends cp -r "${dir}/." "${tmpdir}" || exit - copy_example_or_examplar_to_solution "${tmpdir}" + copy_example_or_exemplar_to_solution "${tmpdir}" run_tests "${slug}" "${tmpdir}" || { cat "${tmpdir}/results.json"; exit 1; } ) } From 156f9bf7fab6589b025f0d995cff124a6edbce5c Mon Sep 17 00:00:00 2001 From: Eric Willigers Date: Tue, 14 Apr 2026 10:25:15 +1000 Subject: [PATCH 5/5] verify-exercises-in-docker.ps1 --- bin/verify-exercises-in-docker.ps1 | 117 +++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 bin/verify-exercises-in-docker.ps1 diff --git a/bin/verify-exercises-in-docker.ps1 b/bin/verify-exercises-in-docker.ps1 new file mode 100644 index 0000000..d932f22 --- /dev/null +++ b/bin/verify-exercises-in-docker.ps1 @@ -0,0 +1,117 @@ +# 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.ps1 + +# Example: verify single exercise in Docker +# .\bin\verify-exercises-in-docker.ps1 two-fer + +# Example: verify all exercises against specified test runner +# .\bin\verify-exercises-in-docker.ps1 -Image my-local-image + +param( + [Parameter(Position=0, ValueFromRemainingArguments)] + [string[]]$Slugs, + [string]$Image = "" +) + +$ErrorActionPreference = "Stop" + +Function Test-RequiredTool([string]$Name) { + if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) { + Write-Error "${Name} is required but not installed. Please install it and make sure it's in your PATH." + } +} + +Function Copy-ReferenceSolution([string]$Dir) { + $config = Get-Content (Join-Path $Dir ".meta" "config.json") | ConvertFrom-Json + $solutions = @($config.files.solution) + $sources = @(if ($config.files.exemplar) { $config.files.exemplar } else { $config.files.example }) + for ($i = 0; $i -lt $solutions.Count; $i++) { + $dst = Join-Path $Dir $solutions[$i] + $src = Join-Path $Dir $sources[$i] + Copy-Item -Path $src -Destination $dst -Force + } +} + +Function Invoke-Tests([string]$Slug, [string]$Dir) { + $dockerArgs = @( + "run", "--rm", "--network", "none", + "--mount", "type=bind,src=${Dir},dst=/solution", + "--mount", "type=volume,dst=/tmp", + $Image, $Slug, "/solution", "/solution" + ) + docker @dockerArgs | Out-Null + $resultsPath = Join-Path $Dir "results.json" + $results = Get-Content $resultsPath | ConvertFrom-Json + return $results.status -eq "pass" +} + +Function Test-Exercise([string]$ExerciseDir) { + $dir = (Resolve-Path $ExerciseDir).Path + $slug = Split-Path $dir -Leaf + $tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) "exercism-verify-${slug}-$([System.IO.Path]::GetRandomFileName())" + New-Item -ItemType Directory -Path $tmpDir | Out-Null + + Write-Host "Verifying ${slug} exercise..." + try { + Copy-Item -Path (Join-Path $dir "*") -Destination $tmpDir -Recurse -Force + Copy-ReferenceSolution $tmpDir + $passed = Invoke-Tests $slug $tmpDir + if (-not $passed) { + Get-Content (Join-Path $tmpDir "results.json") | Write-Host + return $false + } + return $true + } + finally { + Remove-Item -Path $tmpDir -Recurse -Force -ErrorAction SilentlyContinue + } +} + +# Main + +Test-RequiredTool "docker" +Test-RequiredTool "jq" + +if (-not $Image) { + $Image = "exercism/racket-test-runner" + docker pull $Image + if ($LASTEXITCODE -ne 0) { + Write-Error "docker pull ${Image} failed. Check the test runner docs at https://exercism.org/docs/building/tooling/test-runners for more information." + } +} + +$exercises = @() +if ($Slugs.Count -gt 0) { + foreach ($slug in $Slugs) { + foreach ($parent in "concept", "practice") { + $path = Join-Path "." "exercises" $parent $slug + if (Test-Path $path -PathType Container) { + $exercises += $path + } + } + } +} +else { + foreach ($parent in "concept", "practice") { + $parentDir = Join-Path "." "exercises" $parent + if (Test-Path $parentDir -PathType Container) { + $exercises += Get-ChildItem -Path $parentDir -Directory | ForEach-Object { $_.FullName } + } + } +} + +if ($exercises.Count -eq 0) { + Write-Error "No matching exercises found" +} + +$rc = 0 +foreach ($exerciseDir in $exercises) { + $passed = Test-Exercise $exerciseDir + if (-not $passed) { $rc = 1 } +} +exit $rc