From 0571a90862d0e5202cf6dcbf77e9a9c53e01ce0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Wed, 31 Jul 2024 09:06:01 -0700 Subject: [PATCH] Tooling to generate new exercises --- .gitignore | 5 +- Makefile | 40 +++++++--- bin/generate-all-exercises | 67 ++++++++++++++++ bin/generate-stub-program | 21 +++++ bin/generate-test-file | 51 ++++++++++++ bin/new-exercise | 160 +++++++++++++++++++++---------------- bin/verify-exercises | 35 ++------ 7 files changed, 269 insertions(+), 110 deletions(-) create mode 100755 bin/generate-all-exercises create mode 100755 bin/generate-stub-program create mode 100755 bin/generate-test-file diff --git a/.gitignore b/.gitignore index 07136ae..a7e9dcc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,10 @@ /bin/configlet.exe /bin/shellcheck /bin/ys* +/done/ +/new/ /note/ /shellcheck* -/.clojure/ /out.txt +/.clojure/ +/.problem/ diff --git a/Makefile b/Makefile index 32afd30..54caf4f 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,6 @@ CHECKS := \ check-shell \ check-exercism \ check-verify \ - test-exercises \ GEN_FILES := \ $(EXERCISE_MAKEFILES) \ @@ -39,10 +38,15 @@ GEN_FILES := \ SHELL_FILES := \ $(BIN)/fetch-configlet \ + $(BIN)/new-exercise \ + $(BIN)/verify-exercises \ YAML_FILES := \ config.yaml \ +PROBLEM_REPO := .problem +PROBLEM_REPO_URL := https://github.com/exercism/problem-specifications + CLOJURE_REPO := .clojure CLOJURE_REPO_URL := https://github.com/exercism/clojure @@ -56,7 +60,7 @@ SHELLCHECK_RELEASE := \ LINE := $(shell printf '%.0s-' {1..80}) -exercise ?= +slug ?= test ?= v ?= @@ -65,7 +69,7 @@ test-name := all exercises override test := exercises/practice/*/.meta/test/*.ys else test-name := $(test) -override test := exercises/practice/$(test)/test/.meta/*.ys +override test := exercises/practice/$(test)/.meta/test/*.ys endif export YSPATH := $(shell IFS=:; p=$aexercises/practice/*/.meta$b; \ @@ -75,11 +79,15 @@ export YSPATH := $(shell IFS=:; p=$aexercises/practice/*/.meta$b; \ #------------------------------------------------------------------------------ default: -new: $(CFGLET) $(YS) $(CLOJURE_REPO) -ifndef exercise - $(error Please set the 'exercise' variable) +.PHONY: new +new: $(CFGLET) $(YS) $(PROBLEM_REPO) $(CLOJURE_REPO) +ifndef slug + $(error Please set the 'slug' variable) endif - exercise=$(exercise) new-exercise + @exercise=$(slug) new-exercise + +generate-all: + generate-all-exercises $(slug) $(slugs) check: $(YS) $(CHECKS) @echo $(LINE) @@ -94,9 +102,12 @@ deps: $(YS) $(CFGLET) $(SHELLCHECK) test-exercises: $(YS) @echo $(LINE) @echo '*** Running tests for $(test-name)' - prove $(if $v,-v ,)$(test) + @$(MAKE) --no-print-directory run-tests @echo '*** All exercises test ok' +run-tests: + prove $(if $v,-v ,)$(test) + check-yaml: $(YS) $(YAML_FILES) @echo $(LINE) @echo '*** Test YAML files' @@ -109,7 +120,7 @@ check-yaml: $(YS) $(YAML_FILES) check-shell: $(SHELLCHECK) @echo $(LINE) @echo '*** Test shell script files' - @$< $(SHELL_FILES) + $< $(SHELL_FILES) @echo '*** Shell script files are OK' @echo @@ -123,7 +134,7 @@ check-exercism: $(CFGLET) update check-verify: update @echo $(LINE) @echo '*** Test all exercises are verified' - $(VERIFY) $(test) + $(VERIFY) $(slug) @echo '*** All exercises are verified OK' @echo @@ -134,7 +145,11 @@ realclean: clean $(RM) $(CFGLET) $(RM) $(SHELLCHECK) $(RM) $(BIN)/ys* - $(RM) -r $(CLOJURE_REPO) + $(RM) -r $(CLOJURE_REPO) $(PROBLEM_REPO) + +reset: + git checkout -- config.* + git status -- exercises | grep exercises | xargs rm -fr exercises/practice/%/Makefile: common/exercise.mk cp -p $< $@ @@ -146,6 +161,9 @@ $(EXERCISE_META_TESTS): %.json: %.yaml $(YS) Makefile $(YS) -l $< | jq > $@ +$(PROBLEM_REPO): + git clone $(PROBLEM_REPO_URL) $@ + $(CLOJURE_REPO): git clone $(CLOJURE_REPO_URL) $@ diff --git a/bin/generate-all-exercises b/bin/generate-all-exercises new file mode 100755 index 0000000..d5e807c --- /dev/null +++ b/bin/generate-all-exercises @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +set -euo pipefail + +P=.problem/exercises +E=exercises/practice + +if [[ $# -gt 0 ]]; then + SLUGS=("$@") +else + for exercise in $P/*; do + SLUGS+=($(basename "$exercise")) + done +fi + +i=0 +for exercise in "${SLUGS[@]}"; do + slug=$(basename "$exercise") + CDJ=$P/$slug/canonical-data.json + branch=exercise-$slug + new=new/$branch + + if [[ $slug == armstrong-numbers ]]; then + echo "SKIP - '$slug' triggers a ys bug" + continue + fi + if [[ -e $E/$slug ]]; then + echo "SKIP - directory '$E/$slug' already exists" + continue + fi + if git show-ref --quiet "refs/heads/$branch"; then + echo "SKIP - branch 'exercise-$slug' already exists" + continue + fi + if [[ -e $P/$slug/.deprecated ]]; then + echo "SKIP - exercise '$slug' is deprecated" + continue + fi + if [[ ! -e $CDJ ]]; then + echo "SKIP - '$CDJ' not found" + continue + fi + if [[ $(jq '.cases[0].property' "$CDJ") == null ]]; then + echo "SKIP - '$slug' has multiple property values" + continue + fi + + make --no-print-directory new slug="$slug" || { + echo "FAILED - to generate new exercise '$slug'" + rm -fr "$E/$slug" + continue + } + + git branch "$branch" HEAD + git worktree add -f "$new" "$branch" + cp config.* "$new" + mv "$E/$slug" "$new/$E/" + git -C "$new/$E" add . + git -C "$new/$E" commit -m "Implement exercise '$slug'" + + make reset + + echo "*** Generated new exercise '$slug'" + + i=$((i++)) + [[ $i -lt ${COUNT:-9999} ]] || break +done diff --git a/bin/generate-stub-program b/bin/generate-stub-program new file mode 100755 index 0000000..7b755b7 --- /dev/null +++ b/bin/generate-stub-program @@ -0,0 +1,21 @@ +#!/usr/bin/env ys-0 + +defn main(slug): + data =: + json/load: + slurp: ".problem/exercises/$slug/canonical-data.json" + + case =: data.cases.0 + func =: case.property.kebab-symbol() + args =: + join ' ': case.input.keys().mapv(kebab-symbol) + + say: |- + !yamlscript/v0 + + defn ${func}($args): + # Implement the '$func' function. + +defn kebab-symbol(v): + replace v /[A-Z]/: + fn [c]: +"-" + c.lower-case() diff --git a/bin/generate-test-file b/bin/generate-test-file new file mode 100755 index 0000000..83eaf02 --- /dev/null +++ b/bin/generate-test-file @@ -0,0 +1,51 @@ +#!/usr/bin/env ys-0 + +defn main(slug): + data =: + json/load: + slurp: ".problem/exercises/$slug/canonical-data.json" + + tests =: + loop [[case & cases] data.cases, tests []]: + if cases.seq(): + recur cases: tests.conj(gen-test(case)) + =>: tests + + test =: | + #!/usr/bin/env ys-0 + + require ys::taptest: :all + + use: $slug + $gen-comments(data) + test:: + $(join("\n" tests).trimr()) + + done: # $count(tests) + + say: test + +defn gen-test(case): + test =:: + - name:: case.description.capitalize() + code:: gen-code(case) + want:: case.expected + uuid:: case.uuid + SKIP: true + + yaml/dump: test + +defn gen-code(case): + "$(case.property)($(join ' ' case.input.vals().mapv(pr-str)))" + +defn gen-comments(data): + if data.comments: + then: + comments =: + data + .comments + .map(trim) + .join("\n") + .replace(/(?m)^/ '# ') + =>: "\n$comments\n" + else: '' diff --git a/bin/new-exercise b/bin/new-exercise index dede038..74e66c4 100755 --- a/bin/new-exercise +++ b/bin/new-exercise @@ -2,94 +2,112 @@ set -euo pipefail -die() { - echo "Error: $*" >&2 - exit 1 -} +main() ( + setup "$@" -[[ ${DEBUG-} ]] && set -x + configlet create --practice-exercise="$slug" >/dev/null + + update-config-yaml + + generate-example-program + + generate-stub-program -[[ $exercise ]] || - die "The 'exercise' variable not set" - -slug=$exercise -clj=.clojure/exercises/practice/$slug -ys=exercises/practice/$slug -_slug=${slug//-/_} - -clj_example_file=$clj/.meta/src/example.clj -ys_example_file=$ys/.meta/$slug.ys -clj_stub_file=$clj/src/$_slug.clj -ys_stub_file=$ys/$slug.ys -clj_test_file=$clj/test/${_slug}_test.clj -ys_test_file=$ys/test/test-1.ys - -[[ -d $ys ]] && - die "Directory '$ys' already exists" - -configlet create --practice-exercise="$slug" - -new_yaml=$(mktemp) -ys -Y config.json > "$new_yaml" -new_section=$( - diff -u config.yaml "$new_yaml" | - perl -p0e ' - s{(?s:.*)\n(\+\ \ -\ slug.*\n(?:\+.*\n)+)(?s:.*)}{$1}; - s/^\+//mg' || true + generate-test-file + + make-update + + # show-new-exercise ) -top_section=$(grep -B999 '^tags:' config.yaml | head -n -2) -bottom_section=$(grep -A999 '^tags:' config.yaml) -# Update config.yaml: -( - cat <<... +setup() { + [[ $exercise ]] || + die "The 'exercise' variable not set" + + slug=$exercise + prob=.problem/exercises/$slug + clj=.clojure/exercises/practice/$slug + ys=exercises/practice/$slug + meta=$ys/.meta + _slug=${slug//-/_} + + [[ ! -d $ys ]] || + die "Directory '$ys' already exists" + + clj_example_file=$clj/.meta/src/example.clj + ys_example_file=$meta/$slug.ys + clj_stub_file=$clj/src/$_slug.clj + ys_stub_file=$ys/$slug.ys + # clj_test_file=$clj/test/${_slug}_test.clj + ys_test_file=$ys/test/test-1.ys +} + +update-config-yaml() ( + new_yaml=$(mktemp) + ys -Y config.json > "$new_yaml" + new_section=$( + diff -u config.yaml "$new_yaml" | + perl -p0e ' + s{(?s:.*)\n(\+\ \ -\ slug.*\n(?:\+.*\n)+)(?s:.*)}{$1}; + s/^\+//mg' || true + ) + top_section=$(grep -B999 '^tags:' config.yaml | head -n -2) + bottom_section=$(grep -A999 '^tags:' config.yaml) + + # Update config.yaml: + ( + cat <<... $top_section $new_section $bottom_section ... -) > config.yaml - -# Generate example program from Clojure equivalent: -( - cat <<... -!yamlscript/v0 - -$(perl -pe 's/^(.)/# $1/' < "$clj_example_file") -... -) > "$ys_example_file" - -# Generate stub program from Clojure equivalent: -( - cat <<... -!yamlscript/v0 + ) > config.yaml +) -$(perl -pe 's/^(.)/# $1/' < "$clj_stub_file") -... -) > "$ys_stub_file" +generate-example-program() ( + ( + command generate-stub-program "$slug" + if [[ -e $clj_example_file ]]; then + echo + perl -pe 's/^(.)/# $1/' < "$clj_example_file" + fi + ) > "$ys_example_file" +) -# Generate test file from Clojure equivalent: -( - cat <<... -#!/usr/bin/env ys-0 +generate-stub-program() ( + ( + command generate-stub-program "$slug" + ) > "$ys_stub_file" +) -require ys::taptest: :all +generate-test-file() ( + command generate-test-file "$slug" \ + > "$ys_test_file" +) -use: $slug +make-update() ( + touch common/exercise.mk -test:: [] + make update >/dev/null +) -$(perl -pe 's/^(.)/# $1/' < "$clj_test_file") +show-new-exercise() ( + if [[ $(command -v tree) ]]; then + tree -a "$ys" + fi +) -done: -... -) > "$ys_test_file" +warn() ( + echo "$*" >&2 +) -touch common/exercise.mk +die() { + echo "Error: $*" >&2 + exit 1 +} -make update +[[ ${DEBUG-} ]] && set -x -if [[ $(command -v tree) ]]; then - tree -a "$ys" -fi +main "$@" diff --git a/bin/verify-exercises b/bin/verify-exercises index 1a93da6..2b4cd4a 100755 --- a/bin/verify-exercises +++ b/bin/verify-exercises @@ -2,39 +2,20 @@ # Synopsis: # Test the track's exercises. -# -# At a minimum, this file must check if the example/exemplar solution of each -# Practice/Concept Exercise passes the exercise's tests. -# -# To check this, you usually have to (temporarily) replace the exercise's solution files -# with its exemplar/example files. # -# If your track uses skipped tests, make sure to (temporarily) enable these tests -# before running the tests. +# This file must check if the example/exemplar solution of each +# Practice/Concept Exercise passes the exercise's tests. # -# The path to the solution/example/exemplar files can be found in the exercise's -# .meta/config.json file, or possibly inferred from the exercise's directory name. - # Example: verify all exercises # ./bin/verify-exercises # Example: verify single exercise # ./bin/verify-exercises two-fer -slug="${1:-*}" - -# Verify the Concept Exercises -for concept_exercise_dir in ./exercises/concept/${slug}/; do - if [ -d $concept_exercise_dir ]; then - echo "Checking $(basename "${concept_exercise_dir}") exercise..." - # TODO: run command to verify that the exemplar solution passes the tests - fi -done +set -eou pipefail -# Verify the Practice Exercises -for practice_exercise_dir in ./exercises/practice/${slug}/; do - if [ -d $practice_exercise_dir ]; then - echo "Checking $(basename "${practice_exercise_dir}") exercise..." - # TODO: run command to verify that the example solution passes the tests - fi -done +if [[ ${1-} ]]; then + exercise=$1 make run-tests +else + make --no-print-directory run-tests +fi