From df762f8cf045703b39980782a9568cd950494b25 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Mar 2026 09:45:11 +0100 Subject: [PATCH 1/7] add template files that are needed for each exercise --- templates/.gitignore | 1 + templates/rescript.json | 15 +++++++++++++++ templates/testTemplate.js | 17 +++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 templates/.gitignore create mode 100644 templates/rescript.json create mode 100644 templates/testTemplate.js diff --git a/templates/.gitignore b/templates/.gitignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/templates/.gitignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/templates/rescript.json b/templates/rescript.json new file mode 100644 index 0000000..3db8c50 --- /dev/null +++ b/templates/rescript.json @@ -0,0 +1,15 @@ +{ + "name": "@exercism/rescript", + "sources": [ + { "dir": "src", "subdirs": true, "type": "dev" }, + { "dir": "tests", "subdirs": true, "type": "dev" } + ], + "package-specs": [ + { + "module": "esmodule", + "in-source": true + } + ], + "suffix": ".res.js", + "dev-dependencies": ["rescript-test"] +} diff --git a/templates/testTemplate.js b/templates/testTemplate.js new file mode 100644 index 0000000..0db698e --- /dev/null +++ b/templates/testTemplate.js @@ -0,0 +1,17 @@ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { stringEqual } from "../../../../test_generator/assertions.js"; +import { generateTests } from '../../../../test_generator/testGenerator.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const slug = path.basename(path.resolve(__dirname, '..')) + +// EDIT THIS WITH YOUR ASSERTIONS +export const assertionFunctions = [ stringEqual ] + +// EDIT THIS WITH YOUR TEST TEMPLATES +export const template = (c) => { + return `stringEqual(~message="${c.description}", hello(), "${c.expected}")` +} + +generateTests(__dirname, slug, assertionFunctions, template) \ No newline at end of file From 0aad7742488ad5354748de27cf44550e11f914db Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Mar 2026 09:45:36 +0100 Subject: [PATCH 2/7] create new commands to copy exercise template files to the exercise directories --- Makefile | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index ab6cf62..7064752 100644 --- a/Makefile +++ b/Makefile @@ -20,13 +20,27 @@ check-package-files: copy-package-file: @cp package.json exercises/practice/$(EXERCISE)/package.json @cp package-lock.json exercises/practice/$(EXERCISE)/package-lock.json - @cp rescript.json exercises/practice/$(EXERCISE)/rescript.json - + @cp templates/rescript.json exercises/practice/$(EXERCISE)/rescript.json + # copy package files to all exercise directories sync-package-files: @echo "Syncing package.json and package-lock.json..." @for exercise in $(EXERCISES); do EXERCISE=$$exercise $(MAKE) -s copy-package-file || exit 1; done +# copy all relevant files for a single exercise - test template, config etc. +copy-exercise-files: + @cp package.json exercises/practice/$(EXERCISE)/package.json + @cp package-lock.json exercises/practice/$(EXERCISE)/package-lock.json + @cp templates/rescript.json exercises/practice/$(EXERCISE)/rescript.json + @cp templates/.gitignore exercises/practice/$(EXERCISE)/.gitignore + @cp LICENSE exercises/practice/$(EXERCISE)/LICENSE + @cp templates/testTemplate.js exercises/practice/$(EXERCISE)/.meta/testTemplate.js + +# sync all files for each exercise directory +sync-exercise-files: + @echo "Syncing exercise files..." + @for exercise in $(EXERCISES); do EXERCISE=$$exercise $(MAKE) -s copy-exercise-files || exit 1; done + copy-exercise: if [ -f exercises/practice/$(EXERCISE)/src/*.res ]; then \ echo "Copying $(EXERCISE)"; \ From a62ec21d7bfbb3e7d3afc97254423392453ebd92 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Mar 2026 10:01:09 +0100 Subject: [PATCH 3/7] copy exercise files from templates directory after configlet has created the exercise --- bin/add-practice-exercise | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/add-practice-exercise b/bin/add-practice-exercise index 7c86306..6283caf 100755 --- a/bin/add-practice-exercise +++ b/bin/add-practice-exercise @@ -70,6 +70,8 @@ fi exercise_dir="exercises/practice/${slug}" 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") +make copy-exercise-files EXERCISE="${slug}" + cat << NEXT_STEPS Your next steps are: From 347b9d644479df268e4f1794fc7b142d62e1ac8f Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Mar 2026 10:01:49 +0100 Subject: [PATCH 4/7] do not ignore student .gitignore files --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 786b71a..6d6c441 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ bin/configlet.zip node_modules tmp -exercises/**/.gitignore lib From 4429ba122bbe0961de953908efbb64ccc568d545 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Mar 2026 13:16:02 +0100 Subject: [PATCH 5/7] add md5-hash checker --- bin/md5-hash | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100755 bin/md5-hash diff --git a/bin/md5-hash b/bin/md5-hash new file mode 100755 index 0000000..f02ecf4 --- /dev/null +++ b/bin/md5-hash @@ -0,0 +1,21 @@ +#!/bin/bash +# +# Calculates the MD5 hash of a given file. It uses hashing utilities powered by +# operating systems, but wraps them into a consistent interface. + +OS=$( + case $(uname) in + (Darwin*) echo "mac";; + (Linux*) echo "linux";; + # TODO: implement MD5 hashing on Windows + # (Windows*) echo "windows";; + (*) echo "linux";; + esac +) + +case $OS in + mac ) + md5 -q $@;; + linux ) + md5sum $@ | sed -E 's/([a-z0-9]{32}).+$/\1/';; +esac From 6b4b44fec3d75a559c5f9a3fa3568e05b34aacc4 Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Mar 2026 14:51:04 +0100 Subject: [PATCH 6/7] revert to using diff to simply check that the file is the same, even if the hash is different --- Makefile | 52 ++++++++++++++++++++++++++++++++-------------------- bin/md5-hash | 21 --------------------- 2 files changed, 32 insertions(+), 41 deletions(-) delete mode 100755 bin/md5-hash diff --git a/Makefile b/Makefile index 7064752..6d4d750 100644 --- a/Makefile +++ b/Makefile @@ -4,28 +4,36 @@ EXERCISE ?= "" EXERCISES = $(shell find ./exercises/practice -maxdepth 1 -mindepth 1 -type d | cut -s -d '/' -f4 | sort) OUTDIR ?= "tmp" -# check all package.json and package-lock.json are matching -check-package-files: - @echo "Validation package.json files..." - @for pkg in $(PKG_FILES); do \ - ! ./bin/md5-hash $$pkg | grep -qv $(SOURCE_PKG_MD5) || { echo "$$pkg does not match main package.json. Please run 'make sync-package-files' locally and commit the results."; exit 1; }; \ - done - @echo "Validation package-lock.json files..." - @for pkg in $(PKG_LOCK_FILES); do \ - ! ./bin/md5-hash $$pkg | grep -qv $(SOURCE_PKG_LOCK_MD5) || { echo "$$pkg does not match main package.json. Please run 'make sync-package-files' locally and commit the results."; exit 1; }; \ - done - @echo "package-file check complete..." +# Define the files you want to ensure are synced across all exercises +FILES_TO_CHECK = package.json package-lock.json rescript.json .gitignore -# copy package.json and package-lock.json for single exercise -copy-package-file: - @cp package.json exercises/practice/$(EXERCISE)/package.json - @cp package-lock.json exercises/practice/$(EXERCISE)/package-lock.json - @cp templates/rescript.json exercises/practice/$(EXERCISE)/rescript.json +# SOURCE_HASH_package.json = $(shell ./bin/md5-hash ./package.json) +# SOURCE_HASH_package-lock.json = $(shell ./bin/md5-hash ./package-lock.json) +# SOURCE_HASH_rescript.json = $(shell ./bin/md5-hash ./templates/rescript.json) +# SOURCE_HASH_.gitignore = $(shell ./bin/md5-hash ./templates/.gitignore) -# copy package files to all exercise directories -sync-package-files: - @echo "Syncing package.json and package-lock.json..." - @for exercise in $(EXERCISES); do EXERCISE=$$exercise $(MAKE) -s copy-package-file || exit 1; done +check-exercise-files: + @for exercise in $(EXERCISES); do \ + echo "Checking exercise: $$exercise"; \ + for file in $(FILES_TO_CHECK); do \ + target="exercises/practice/$$exercise/$$file"; \ + source="./templates/$$file"; \ + [ -f $$source ] || source="./$$file"; \ + \ + # 1. Check if the file exists \ + if [ ! -f "$$target" ]; then \ + echo "ERROR: Missing file $$file in $$exercise. Run make sync-exercise-files and commit the changes."; \ + exit 1; \ + fi; \ + \ + # 2. Check if the content matches (ignoring name/version) \ + diff -q -I '"name":' -I '"version":' "$$source" "$$target" > /dev/null || { \ + echo "ERROR: $$target has significant differences from $$source."; \ + exit 1; \ + }; \ + done; \ + done + @echo "All exercises contain all required files and are in sync." # copy all relevant files for a single exercise - test template, config etc. copy-exercise-files: @@ -41,6 +49,7 @@ sync-exercise-files: @echo "Syncing exercise files..." @for exercise in $(EXERCISES); do EXERCISE=$$exercise $(MAKE) -s copy-exercise-files || exit 1; done +# copy single exercise build artifacts for testing copy-exercise: if [ -f exercises/practice/$(EXERCISE)/src/*.res ]; then \ echo "Copying $(EXERCISE)"; \ @@ -48,6 +57,7 @@ copy-exercise: cp exercises/practice/$(EXERCISE)/tests/*.res $(OUTDIR)/tests/; \ fi +# copy build artifacts for testing copy-all-exercises: @echo "Copying exercises for testing..." @mkdir -p $(OUTDIR)/src @@ -64,6 +74,7 @@ format: @echo "Formatting ReScript files..." @find . -name "node_modules" -prune -o -name "*.res" -print -o -name "*.resi" -print | xargs npx rescript format +# Generate tests for all exercises generate-tests: @echo "Generating tests for all exercises..." @for exercise in $(EXERCISES); do \ @@ -76,6 +87,7 @@ generate-tests: done @echo "All tests generated successfully." +# Generate test for exercise generate-test: ifeq ($(EXERCISE),"") $(error EXERCISE variable is required. usage: make generate_test EXERCISE=hello-world) diff --git a/bin/md5-hash b/bin/md5-hash deleted file mode 100755 index f02ecf4..0000000 --- a/bin/md5-hash +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash -# -# Calculates the MD5 hash of a given file. It uses hashing utilities powered by -# operating systems, but wraps them into a consistent interface. - -OS=$( - case $(uname) in - (Darwin*) echo "mac";; - (Linux*) echo "linux";; - # TODO: implement MD5 hashing on Windows - # (Windows*) echo "windows";; - (*) echo "linux";; - esac -) - -case $OS in - mac ) - md5 -q $@;; - linux ) - md5sum $@ | sed -E 's/([a-z0-9]{32}).+$/\1/';; -esac From e449b135db25c892a7250e99bbaa777536e00ebd Mon Sep 17 00:00:00 2001 From: Owen Date: Mon, 9 Mar 2026 14:58:19 +0100 Subject: [PATCH 7/7] check for testTemplate inside .meta folder --- Makefile | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 6d4d750..61d83fe 100644 --- a/Makefile +++ b/Makefile @@ -5,20 +5,23 @@ EXERCISES = $(shell find ./exercises/practice -maxdepth 1 -mindepth 1 -type d | OUTDIR ?= "tmp" # Define the files you want to ensure are synced across all exercises -FILES_TO_CHECK = package.json package-lock.json rescript.json .gitignore - -# SOURCE_HASH_package.json = $(shell ./bin/md5-hash ./package.json) -# SOURCE_HASH_package-lock.json = $(shell ./bin/md5-hash ./package-lock.json) -# SOURCE_HASH_rescript.json = $(shell ./bin/md5-hash ./templates/rescript.json) -# SOURCE_HASH_.gitignore = $(shell ./bin/md5-hash ./templates/.gitignore) +FILES_TO_CHECK = package.json package-lock.json rescript.json .gitignore LICENSE .meta/testTemplate.js +# check all exercise files that need to be in sync check-exercise-files: @for exercise in $(EXERCISES); do \ echo "Checking exercise: $$exercise"; \ for file in $(FILES_TO_CHECK); do \ target="exercises/practice/$$exercise/$$file"; \ - source="./templates/$$file"; \ - [ -f $$source ] || source="./$$file"; \ + \ + # Map the source template path \ + if [ "$$file" = ".meta/testTemplate.js" ]; then \ + source="./templates/testTemplate.js"; \ + elif [ -f "./templates/$$file" ]; then \ + source="./templates/$$file"; \ + else \ + source="./$$file"; \ + fi; \ \ # 1. Check if the file exists \ if [ ! -f "$$target" ]; then \ @@ -28,7 +31,8 @@ check-exercise-files: \ # 2. Check if the content matches (ignoring name/version) \ diff -q -I '"name":' -I '"version":' "$$source" "$$target" > /dev/null || { \ - echo "ERROR: $$target has significant differences from $$source."; \ + echo "ERROR: $$target does not match template $$source."; \ + diff -u -I '"name":' -I '"version":' "$$source" "$$target" | head -n 20; \ exit 1; \ }; \ done; \ @@ -96,6 +100,6 @@ endif test: $(MAKE) -s clean - $(MAKE) -s check-package-files + $(MAKE) -s check-exercise-files $(MAKE) -s copy-all-exercises npm run ci \ No newline at end of file