Skip to content

Commit 8199c64

Browse files
authored
Standard scripts copied from generic-track repo (exercism#786)
1 parent a0ae8a1 commit 8199c64

File tree

4 files changed

+301
-0
lines changed

4 files changed

+301
-0
lines changed

bin/add-practice-exercise

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
#!/usr/bin/env bash
2+
3+
# Synopsis:
4+
# Scaffold the files for a new practice exercise.
5+
# After creating the exercise, follow the instructions in the output.
6+
7+
# Example:
8+
# bin/add-practice-exercise two-fer
9+
10+
# Example with difficulty:
11+
# bin/add-practice-exercise -d 5 two-fer
12+
13+
# Example with author and difficulty:
14+
# bin/add-practice-exercise -a foo -d 3 two-fer
15+
16+
set -euo pipefail
17+
scriptname=$0
18+
19+
help_and_exit() {
20+
echo >&2 "Scaffold the files for a new practice exercise."
21+
echo >&2 "Usage: ${scriptname} [-h] [-a author] [-d difficulty] <exercise-slug>"
22+
echo >&2 "Where: author is the GitHub username of the exercise creator."
23+
echo >&2 "Where: difficulty is between 1 (easiest) to 10 (hardest)."
24+
exit 1
25+
}
26+
27+
die() { echo >&2 "$*"; exit 1; }
28+
29+
required_tool() {
30+
command -v "${1}" >/dev/null 2>&1 ||
31+
die "${1} is required but not installed. Please install it and make sure it's in your PATH."
32+
}
33+
34+
require_files_template() {
35+
jq -e --arg key "${1}" '.files[$key] | length > 0' config.json > /dev/null ||
36+
die "The '.files.${1}' array in the 'config.json' file is empty. Please add at least one file. See https://exercism.org/docs/building/tracks/config-json#h-files for more information."
37+
}
38+
39+
required_tool jq
40+
41+
require_files_template "solution"
42+
require_files_template "test"
43+
require_files_template "example"
44+
45+
[[ -f ./bin/fetch-configlet ]] || die "Run this script from the repo's root directory."
46+
47+
author=''
48+
difficulty='1'
49+
while getopts :ha:d: opt; do
50+
case $opt in
51+
h) help_and_exit ;;
52+
a) author=$OPTARG ;;
53+
d) difficulty=$OPTARG ;;
54+
?) echo >&2 "Unknown option: -$OPTARG"; help_and_exit ;;
55+
esac
56+
done
57+
shift "$((OPTIND - 1))"
58+
59+
(( $# >= 1 )) || help_and_exit
60+
61+
slug="${1}"
62+
63+
if [[ -z "${author}" ]]; then
64+
read -rp 'Your GitHub username: ' author
65+
fi
66+
67+
./bin/fetch-configlet
68+
./bin/configlet create --practice-exercise "${slug}" --author "${author}" --difficulty "${difficulty}"
69+
70+
exercise_dir="exercises/practice/${slug}"
71+
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")
72+
73+
cat << NEXT_STEPS
74+
75+
Your next steps are:
76+
- Create the test suite in $(jq -r '.test' <<< "${files}")
77+
- The tests should be based on the canonical data at 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json'
78+
- Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false"
79+
- Create the example solution in $(jq -r '.example' <<< "${files}")
80+
- Verify the example solution passes the tests by running 'bin/verify-exercises ${slug}'
81+
- Create the stub solution in $(jq -r '.solution' <<< "${files}")
82+
- Update the 'difficulty' value for the exercise's entry in the 'config.json' file in the repo's root
83+
- Validate CI using 'bin/configlet lint' and 'bin/configlet fmt'
84+
NEXT_STEPS

bin/fetch-configlet.ps1

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This file is a copy of the
2+
# https://github.com/exercism/configlet/blob/main/scripts/fetch-configlet.ps1 file.
3+
# Please submit bugfixes/improvements to the above file to ensure that all tracks
4+
# benefit from the changes.
5+
6+
$ErrorActionPreference = "Stop"
7+
$ProgressPreference = "SilentlyContinue"
8+
9+
$requestOpts = @{
10+
Headers = If ($env:GITHUB_TOKEN) { @{ Authorization = "Bearer ${env:GITHUB_TOKEN}" } } Else { @{ } }
11+
MaximumRetryCount = 3
12+
RetryIntervalSec = 1
13+
}
14+
15+
$arch = If ([Environment]::Is64BitOperatingSystem) { "x86-64" } Else { "i386" }
16+
$fileName = "configlet_.+_windows_$arch.zip"
17+
18+
Function Get-DownloadUrl {
19+
$latestUrl = "https://api.github.com/repos/exercism/configlet/releases/latest"
20+
Invoke-RestMethod -Uri $latestUrl -PreserveAuthorizationOnRedirect @requestOpts
21+
| Select-Object -ExpandProperty assets
22+
| Where-Object { $_.browser_download_url -match $FileName }
23+
| Select-Object -ExpandProperty browser_download_url
24+
}
25+
26+
$downloadUrl = Get-DownloadUrl
27+
$outputDirectory = "bin"
28+
$outputFile = Join-Path -Path $outputDirectory -ChildPath $fileName
29+
Invoke-WebRequest -Uri $downloadUrl -OutFile $outputFile @requestOpts
30+
Expand-Archive $outputFile -DestinationPath $outputDirectory -Force
31+
Remove-Item -Path $outputFile

bin/verify-exercises

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env bash
2+
3+
# Synopsis:
4+
# Verify that each exercise's example/exemplar solution passes the tests.
5+
# You can either verify all exercises or a single exercise.
6+
7+
# Example: verify all exercises
8+
# bin/verify-exercises
9+
10+
# Example: verify single exercise
11+
# bin/verify-exercises two-fer
12+
13+
set -eo pipefail
14+
15+
die() { echo "$*" >&2; exit 1; }
16+
17+
required_tool() {
18+
command -v "${1}" >/dev/null 2>&1 ||
19+
die "${1} is required but not installed. Please install it and make sure it's in your PATH."
20+
}
21+
22+
required_tool jq
23+
24+
copy_example_or_examplar_to_solution() {
25+
jq -c '[.files.solution, .files.exemplar // .files.example] | transpose | map({src: .[1], dst: .[0]}) | .[]' .meta/config.json \
26+
| while read -r src_and_dst; do
27+
cp "$(jq -r '.src' <<< "${src_and_dst}")" "$(jq -r '.dst' <<< "${src_and_dst}")"
28+
done
29+
}
30+
31+
unskip_tests() {
32+
# shellcheck disable=SC2034
33+
jq -r '.files.test[]' .meta/config.json | while read -r test_file; do
34+
noop # TODO: replace this with the command to unskip the tests.
35+
# Note: this function runs from within an exercise directory.
36+
# Note: the exercise directory is a temporary directory, so feel
37+
# free to modify its (test) files as needed.
38+
# Note: ignore this function if either:
39+
# - skipping tests is not supported, or
40+
# - skipping tests does not require modifying the test files.
41+
# Example: sed -i 's/test.skip/test/g' "${test_file}"
42+
done
43+
}
44+
45+
run_tests() {
46+
noop # TODO: replace this with the command to run the tests for the exercise.
47+
# Note: this function runs from within an exercise directory.
48+
# Note: the exercise directory is a temporary directory, so feel
49+
# free to modify its files as needed.
50+
# Note: return a zero exit code if all tests pass, otherwise non-zero.
51+
# Example: `npm test`
52+
# Example: `python3 -m pytest two_fer_test.py`
53+
}
54+
55+
verify_exercise() {
56+
local dir
57+
local slug
58+
local tmp_dir
59+
60+
dir=$(realpath "${1}")
61+
slug=$(basename "${dir}")
62+
tmp_dir=$(mktemp -d -t "exercism-verify-${slug}-XXXXX")
63+
64+
echo "Verifying ${slug} exercise..."
65+
66+
(
67+
trap 'rm -rf "$tmp_dir"' EXIT # remove tempdir when subshell ends
68+
cp -r "${dir}/." "${tmp_dir}"
69+
cd "${tmp_dir}"
70+
71+
copy_example_or_examplar_to_solution
72+
unskip_tests
73+
run_tests
74+
)
75+
}
76+
77+
verify_exercises() {
78+
local exercise_slug
79+
80+
exercise_slug="${1}"
81+
82+
shopt -s nullglob
83+
count=0
84+
for exercise_dir in ./exercises/{concept,practice}/${exercise_slug}/; do
85+
if [[ -d "${exercise_dir}" ]]; then
86+
verify_exercise "${exercise_dir}"
87+
((++count))
88+
fi
89+
done
90+
((count > 0)) || die 'no matching exercises found!'
91+
}
92+
93+
exercise_slug="${1:-*}"
94+
verify_exercises "${exercise_slug}"

bin/verify-exercises-in-docker

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
#!/usr/bin/env bash
2+
3+
# Synopsis:
4+
# Verify that each exercise's example/exemplar solution passes the tests
5+
# using the track's test runner Docker image.
6+
# You can either verify all exercises or a single exercise.
7+
8+
# Example: verify all exercises in Docker
9+
# bin/verify-exercises-in-docker
10+
11+
# Example: verify single exercise in Docker
12+
# bin/verify-exercises-in-docker two-fer
13+
14+
set -eo pipefail
15+
16+
die() { echo "$*" >&2; exit 1; }
17+
18+
required_tool() {
19+
command -v "${1}" >/dev/null 2>&1 ||
20+
die "${1} is required but not installed. Please install it and make sure it's in your PATH."
21+
}
22+
23+
required_tool docker
24+
25+
copy_example_or_examplar_to_solution() {
26+
jq -c '[.files.solution, .files.exemplar // .files.example] | transpose | map({src: .[1], dst: .[0]}) | .[]' .meta/config.json \
27+
| while read -r src_and_dst; do
28+
cp "$(jq -r '.src' <<< "${src_and_dst}")" "$(jq -r '.dst' <<< "${src_and_dst}")"
29+
done
30+
}
31+
32+
pull_docker_image() {
33+
# shellcheck disable=SC1083
34+
docker pull exercism/{{SLUG}}-test-runner ||
35+
die $'Could not find the `exercism/{{SLUG}}-test-runner` Docker image.\nCheck the test runner docs at https://exercism.org/docs/building/tooling/test-runners for more information.'
36+
}
37+
38+
run_tests() {
39+
local slug
40+
slug="${1}"
41+
42+
# shellcheck disable=SC1083
43+
docker run \
44+
--rm \
45+
--network none \
46+
--read-only \
47+
--mount type=bind,src="${PWD}",dst=/solution \
48+
--mount type=bind,src="${PWD}",dst=/output \
49+
--mount type=tmpfs,dst=/tmp \
50+
exercism/{{SLUG}}-test-runner "${slug}" /solution /output
51+
jq -e '.status == "pass"' "${PWD}/results.json" >/dev/null 2>&1
52+
}
53+
54+
verify_exercise() {
55+
local dir
56+
local slug
57+
local tmp_dir
58+
dir=$(realpath "${1}")
59+
slug=$(basename "${dir}")
60+
tmp_dir=$(mktemp -d -t "exercism-verify-${slug}-XXXXX")
61+
62+
echo "Verifying ${slug} exercise..."
63+
64+
(
65+
trap 'rm -rf "$tmp_dir"' EXIT # remove tempdir when subshell ends
66+
cp -r "${dir}/." "${tmp_dir}"
67+
cd "${tmp_dir}"
68+
69+
copy_example_or_examplar_to_solution
70+
run_tests "${slug}"
71+
)
72+
}
73+
74+
verify_exercises() {
75+
local exercise_slug
76+
exercise_slug="${1}"
77+
78+
shopt -s nullglob
79+
count=0
80+
for exercise_dir in ./exercises/{concept,practice}/${exercise_slug}/; do
81+
if [[ -d "${exercise_dir}" ]]; then
82+
verify_exercise "${exercise_dir}"
83+
((++count))
84+
fi
85+
done
86+
((count > 0)) || die 'no matching exercises found!'
87+
}
88+
89+
pull_docker_image
90+
91+
exercise_slug="${1:-*}"
92+
verify_exercises "${exercise_slug}"

0 commit comments

Comments
 (0)