From 66f711ddaeb32d660e9f771f5333a7efed67296a Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Thu, 9 Jan 2025 16:33:21 +0100 Subject: [PATCH 01/26] Some attempts --- .gitignore | 2 + .../collatz-conjecture/.meta/tests.clj | 60 ++++++++++++++++ .../collatz-conjecture/.meta/tests.template | 8 +++ generator.clj | 68 +++++++++++++++++++ project.clj | 3 +- 5 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 exercises/practice/collatz-conjecture/.meta/tests.clj create mode 100644 exercises/practice/collatz-conjecture/.meta/tests.template create mode 100644 generator.clj diff --git a/.gitignore b/.gitignore index 6c001c652..95a6f5bdf 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ pom.xml.asc concepts/functions-generating-functions/introduction_files/ concepts/functions-generating-functions/*.html exercises/**/*.bak +.problem-specifications/ +.cpcache/ diff --git a/exercises/practice/collatz-conjecture/.meta/tests.clj b/exercises/practice/collatz-conjecture/.meta/tests.clj new file mode 100644 index 000000000..2526ee896 --- /dev/null +++ b/exercises/practice/collatz-conjecture/.meta/tests.clj @@ -0,0 +1,60 @@ +(ns isogram-test + (:require [clojure.test :refer [deftest testing is]] + isogram)) + +(deftest isogram_test_1 + (testing "empty string") + (is (isogram/isogram? ""))) + +(deftest isogram_test_2 + (testing "isogram with only lower case characters") + (is (isogram/isogram? "isogram"))) + +(deftest isogram_test_3 + (testing "word with one duplicated character") + (is (not (isogram/isogram? "eleven")))) + +(deftest isogram_test_4 + (testing "word with one duplicated character from the end of the alphabet") + (is (not (isogram/isogram? "zzyzx")))) + +(deftest isogram_test_5 + (testing "longest reported english isogram") + (is (isogram/isogram? "subdermatoglyphic"))) + +(deftest isogram_test_6 + (testing "word with duplicated character in mixed case") + (is (not (isogram/isogram? "Alphabet")))) + +(deftest isogram_test_7 + (testing "word with duplicated character in mixed case, lowercase first") + (is (not (isogram/isogram? "alphAbet")))) + +(deftest isogram_test_8 + (testing "hypothetical isogrammic word with hyphen") + (is (isogram/isogram? "thumbscrew-japingly"))) + +(deftest isogram_test_9 + (testing "hypothetical word with duplicated character following hyphen") + (is (not (isogram/isogram? "thumbscrew-jappingly")))) + +(deftest isogram_test_10 + (testing "isogram with duplicated hyphen") + (is (isogram/isogram? "six-year-old"))) + +(deftest isogram_test_11 + (testing "made-up name that is an isogram") + (is (isogram/isogram? "Emily Jung Schwartzkopf"))) + +(deftest isogram_test_12 + (testing "duplicated character in the middle") + (is (not (isogram/isogram? "accentor")))) + +(deftest isogram_test_13 + (testing "same first and last characters") + (is (not (isogram/isogram? "angola")))) + +(deftest isogram_test_14 + (testing "word with duplicated character and with two hyphens") + (is (not (isogram/isogram? "up-to-date")))) + diff --git a/exercises/practice/collatz-conjecture/.meta/tests.template b/exercises/practice/collatz-conjecture/.meta/tests.template new file mode 100644 index 000000000..7705b998f --- /dev/null +++ b/exercises/practice/collatz-conjecture/.meta/tests.template @@ -0,0 +1,8 @@ +(ns isogram-test + (:require [clojure.test :refer [deftest testing is]] + isogram)) +{% for test_case in cases %} +(deftest isogram_test_{{forloop.counter}} + (testing "{{test_case.path|join:" - "}}") + {% if test_case.expected %}(is (isogram/isogram? "{{test_case.input.phrase}}"))) {% else %}(is (not (isogram/isogram? "{{test_case.input.phrase}}")))) {% endif %} +{% endfor %} diff --git a/generator.clj b/generator.clj new file mode 100644 index 000000000..bf10c4f23 --- /dev/null +++ b/generator.clj @@ -0,0 +1,68 @@ +#!/usr/bin/env bb + +(require + '[babashka.deps :as deps] + '[babashka.classpath :as cp]) + +(deps/add-deps '{:deps {selmer/selmer {:mvn/version "1.12.61"}}}) + +(require + '[cheshire.core :as json] + '[clojure.java.shell :refer [sh]] + '[clojure.java.io :as io] + '[selmer.parser :refer [render render-file]]) + +(defn error [message] + (println message) + (System/exit 1)) + +(def prob-specs-dir ".problem-specifications") + +(defn sync-prob-specs [] + (if (.isDirectory (io/file prob-specs-dir)) + (sh "git" "pull" :dir prob-specs-dir) + (sh "git" "clone" "https://github.com/exercism/problem-specifications.git" prob-specs-dir))) + +(defn canonical-data [slug] + (let [file (io/file prob-specs-dir "exercises" slug "canonical-data.json")] + (if (.exists file) + (json/parse-stream (io/reader file) keyword) + (error (str "No canonical data found for slug '" slug "'"))))) + +(defn filter-reimplemented [case-nodes] + (let [reimplemented (set (remove nil? (map #(:reimplements %) case-nodes)))] + (remove #(contains? reimplemented (:uuid %)) case-nodes))) + +(defn node->case [node] + (-> node + (assoc :error (get-in node [:expected :error])) + (dissoc :reimplements :comments :scenarios))) + +(defn case-nodes + ([node] (case-nodes node [])) + ([node path] + (let [description (:description node) + children (:cases node) + updated-path (if description (conj path description) path)] + (if children + (mapcat #(case-nodes % updated-path) children) + [(assoc node :path updated-path)])))) + +(defn cases [node] + (->> node + (case-nodes) + (filter-reimplemented) + (map node->case) + (into []))) + +(sync-prob-specs) +(cases (canonical-data "isogram")) + +(def data {:slug "isogram" + :cases (cases (canonical-data "isogram"))}) + +(def template "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.template") +(def tests "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.clj") + +(spit tests (render (slurp template) data)) + \ No newline at end of file diff --git a/project.clj b/project.clj index 1ba2e647f..2b4b526c2 100644 --- a/project.clj +++ b/project.clj @@ -6,4 +6,5 @@ :aliases {"generate" ["run" "-m" "generator"]} :dependencies [[org.clojure/clojure "1.10.0"] [cheshire "5.5.0"] - [stencil "0.5.0"]]) + [stencil "0.5.0"] + [org.clojure/data.json "2.5.1"]]) From 642902c657133c5ef3c7a1714fe5a2cb1f8126d1 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 09:40:11 +0100 Subject: [PATCH 02/26] Move to project --- generators/deps.edn | 3 +++ generator.clj => generators/src/generator.clj | 15 ++++----------- project.clj | 10 ---------- 3 files changed, 7 insertions(+), 21 deletions(-) create mode 100644 generators/deps.edn rename generator.clj => generators/src/generator.clj (86%) delete mode 100644 project.clj diff --git a/generators/deps.edn b/generators/deps.edn new file mode 100644 index 000000000..bdfe6bbc9 --- /dev/null +++ b/generators/deps.edn @@ -0,0 +1,3 @@ +{:deps {org.clojure/data.json {:mvn/version "2.5.1"} + selmer/selmer {:mvn/version "1.12.61"} + io.github.tonsky/toml-clj {:mvn/version "0.1.0"}}} \ No newline at end of file diff --git a/generator.clj b/generators/src/generator.clj similarity index 86% rename from generator.clj rename to generators/src/generator.clj index bf10c4f23..b499e250d 100644 --- a/generator.clj +++ b/generators/src/generator.clj @@ -1,16 +1,9 @@ -#!/usr/bin/env bb - -(require - '[babashka.deps :as deps] - '[babashka.classpath :as cp]) - -(deps/add-deps '{:deps {selmer/selmer {:mvn/version "1.12.61"}}}) - (require - '[cheshire.core :as json] + '[clojure.data.json :as json] '[clojure.java.shell :refer [sh]] '[clojure.java.io :as io] - '[selmer.parser :refer [render render-file]]) + '[selmer.parser :refer [render render-file]] + '[toml-clj.core :as toml]) (defn error [message] (println message) @@ -26,7 +19,7 @@ (defn canonical-data [slug] (let [file (io/file prob-specs-dir "exercises" slug "canonical-data.json")] (if (.exists file) - (json/parse-stream (io/reader file) keyword) + (json/read (io/reader file) :key-fn keyword) (error (str "No canonical data found for slug '" slug "'"))))) (defn filter-reimplemented [case-nodes] diff --git a/project.clj b/project.clj deleted file mode 100644 index 2b4b526c2..000000000 --- a/project.clj +++ /dev/null @@ -1,10 +0,0 @@ -(defproject clojure "0.1.0" - :description "Exercism Exercises in Clojure" - :url "https://github.com/exercism/clojure" - :test-paths ["_test"] - :source-paths ["_src"] - :aliases {"generate" ["run" "-m" "generator"]} - :dependencies [[org.clojure/clojure "1.10.0"] - [cheshire "5.5.0"] - [stencil "0.5.0"] - [org.clojure/data.json "2.5.1"]]) From 745196f612def75ca432440e0cf4aabfad1b9267 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 10:15:07 +0100 Subject: [PATCH 03/26] Use tests.toml --- generators/src/generator.clj | 63 ++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/generators/src/generator.clj b/generators/src/generator.clj index b499e250d..4b89bffbc 100644 --- a/generators/src/generator.clj +++ b/generators/src/generator.clj @@ -9,47 +9,62 @@ (println message) (System/exit 1)) -(def prob-specs-dir ".problem-specifications") +(def root-dir (.getCanonicalPath (io/file *file* ".." ".." ".."))) +(def prob-specs-dir (io/file root-dir ".problem-specifications")) +(defn exercise-dir [slug] (io/file root-dir "exercises" "practice" slug)) (defn sync-prob-specs [] - (if (.isDirectory (io/file prob-specs-dir)) + (if (.isDirectory prob-specs-dir) (sh "git" "pull" :dir prob-specs-dir) (sh "git" "clone" "https://github.com/exercism/problem-specifications.git" prob-specs-dir))) +(defn canonical-data-file [slug] (io/file prob-specs-dir "exercises" slug "canonical-data.json")) (defn canonical-data [slug] - (let [file (io/file prob-specs-dir "exercises" slug "canonical-data.json")] + (let [file (canonical-data-file slug)] (if (.exists file) (json/read (io/reader file) :key-fn keyword) - (error (str "No canonical data found for slug '" slug "'"))))) + (error (str "No canonical-data.json found for exercise '" slug "'"))))) -(defn filter-reimplemented [case-nodes] - (let [reimplemented (set (remove nil? (map #(:reimplements %) case-nodes)))] - (remove #(contains? reimplemented (:uuid %)) case-nodes))) +(defn tests-toml-file [slug] (io/file (exercise-dir slug) ".meta" "tests.toml")) +(defn excluded-uuids [slug] + (let [file (tests-toml-file slug)] + (if (.exists file) + (->> file + (io/reader) + (toml/read) + (filter #(= false (get (last %) "include"))) + (map first) + (set)) + (error (str "No tests.toml data found for exercise '" slug "'"))))) + +(defn excluded? [slug] + (let [excluded (excluded-uuids slug)] + (fn [node] (contains? excluded (:uuid node))))) -(defn node->case [node] +(defn node->test-case [node path] (-> node - (assoc :error (get-in node [:expected :error])) + (assoc :path path :error (get-in node [:expected :error])) (dissoc :reimplements :comments :scenarios))) -(defn case-nodes - ([node] (case-nodes node [])) +(defn test-case-nodes + ([node] (test-case-nodes node [])) ([node path] (let [description (:description node) children (:cases node) updated-path (if description (conj path description) path)] (if children - (mapcat #(case-nodes % updated-path) children) - [(assoc node :path updated-path)])))) - -(defn cases [node] - (->> node - (case-nodes) - (filter-reimplemented) - (map node->case) + (mapcat #(test-case-nodes % updated-path) children) + [(node->test-case node updated-path)])))) + +(defn test-cases [slug] + (->> slug + (canonical-data) + (test-case-nodes) + (remove (excluded? slug)) (into []))) (sync-prob-specs) -(cases (canonical-data "isogram")) +(test-cases "isogram") (def data {:slug "isogram" :cases (cases (canonical-data "isogram"))}) @@ -57,5 +72,11 @@ (def template "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.template") (def tests "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.clj") +(def toml-file "/home/erik/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.toml") + (spit tests (render (slurp template) data)) - \ No newline at end of file +(def t (toml/read (io/reader toml-file))) +(set (map first (filter #(not= false (get (last %) "include")) t))) + +(.getAbsolutePath (io/file *file* ".." "..")) +(.getCanonicalPath (io/file *file* ".." ".." "..")) \ No newline at end of file From 93217a581e82c771acf7bfb7d2bedfae447528ba Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 11:07:25 +0100 Subject: [PATCH 04/26] Lots more work --- .../{tests.template => generator.template} | 0 generators/deps.edn | 3 +- generators/src/canonical-data.clj | 68 +++++++++++++++++++ generators/src/log.clj | 5 ++ generators/src/paths.clj | 14 ++++ generators/src/render.clj | 18 +++++ 6 files changed, 107 insertions(+), 1 deletion(-) rename exercises/practice/collatz-conjecture/.meta/{tests.template => generator.template} (100%) create mode 100644 generators/src/canonical-data.clj create mode 100644 generators/src/log.clj create mode 100644 generators/src/paths.clj create mode 100644 generators/src/render.clj diff --git a/exercises/practice/collatz-conjecture/.meta/tests.template b/exercises/practice/collatz-conjecture/.meta/generator.template similarity index 100% rename from exercises/practice/collatz-conjecture/.meta/tests.template rename to exercises/practice/collatz-conjecture/.meta/generator.template diff --git a/generators/deps.edn b/generators/deps.edn index bdfe6bbc9..8a917fb88 100644 --- a/generators/deps.edn +++ b/generators/deps.edn @@ -1,3 +1,4 @@ {:deps {org.clojure/data.json {:mvn/version "2.5.1"} selmer/selmer {:mvn/version "1.12.61"} - io.github.tonsky/toml-clj {:mvn/version "0.1.0"}}} \ No newline at end of file + io.github.tonsky/toml-clj {:mvn/version "0.1.0"} + clj-jgit/clj-jgit {:mvn/version "1.1.0"}}} \ No newline at end of file diff --git a/generators/src/canonical-data.clj b/generators/src/canonical-data.clj new file mode 100644 index 000000000..2a89001f6 --- /dev/null +++ b/generators/src/canonical-data.clj @@ -0,0 +1,68 @@ +(ns canonical-data + (:require [clojure.data.json :as json] + [clojure.java.io :as io] + [clojure.java.shell :refer [sh]] + [toml-clj.core :as toml] + [clj-jgit.porcelain :refer [git-clone git-pull load-repo]] + [log] + [paths])) + +(def git-url "https://github.com/exercism/problem-specifications.git") + +(defn pull-repo [] + (-> paths/prob-specs-dir + (load-repo) + (git-pull))) + +(defn clone-repo [] (git-clone git-url :branch "main" :dir paths/prob-specs-dir)) + +(defn sync-repo [] + (try + (pull-repo) + (catch java.io.FileNotFoundException _ (clone-repo)))) + +(defn canonical-data [slug] + (let [file (paths/canonical-data-file slug)] + (if (.exists file) + (json/read (io/reader file) :key-fn keyword) + (log/error (str "No canonical-data.json found for exercise '" slug "'"))))) + +(defn excluded-uuids [slug] + (let [file (paths/tests-toml-file slug)] + (if (.exists file) + (->> file + (io/reader) + (toml/read) + (filter #(= false (get (last %) "include"))) + (map first) + (set)) + (log/error (str "No tests.toml data found for exercise '" slug "'"))))) + +(defn excluded? [slug] + (let [excluded (excluded-uuids slug)] + (fn [node] (contains? excluded (:uuid node))))) + +(defn node->test-case [node path] + (-> node + (assoc :path path :error (get-in node [:expected :error])) + (dissoc :reimplements :comments :scenarios))) + +(defn test-case-nodes + ([node] (test-case-nodes node [])) + ([node path] + (let [description (:description node) + children (:cases node) + updated-path (if description (conj path description) path)] + (if children + (mapcat #(test-case-nodes % updated-path) children) + [(node->test-case node updated-path)])))) + +(defn test-cases [slug] + (->> slug + (canonical-data) + (test-case-nodes) + (remove (excluded? slug)) + (into []))) + +(sync-prob-specs) +(test-cases "isogram") diff --git a/generators/src/log.clj b/generators/src/log.clj new file mode 100644 index 000000000..b956a9a79 --- /dev/null +++ b/generators/src/log.clj @@ -0,0 +1,5 @@ +(ns log) + +(defn error [message] + (println message) + (System/exit 1)) diff --git a/generators/src/paths.clj b/generators/src/paths.clj new file mode 100644 index 000000000..ed3abc177 --- /dev/null +++ b/generators/src/paths.clj @@ -0,0 +1,14 @@ +(ns paths + (:require [clojure.java.io :as io] + [clojure.string :as str])) + +(def generators-dir (.getCanonicalPath (io/file *file* ".." ".."))) +(def root-dir (.getCanonicalPath (io/file generators-dir ".."))) +(def prob-specs-dir (io/file generators-dir ".problem-specifications")) +(def exercises-dir (io/file root-dir "exercises" "practice")) +(defn exercise-dir [slug] (io/file exercises-dir slug)) +(defn canonical-data-file [slug] (io/file prob-specs-dir "exercises" slug "canonical-data.json")) +(defn tests-toml-file [slug] (io/file (exercise-dir slug) ".meta" "tests.toml")) +(defn generator-template-file [slug] (io/file (exercise-dir slug) ".meta" "generator.template")) +(defn tests-file-name [slug] (str "test_" (str/replace slug "-" "_") ".clj")) +(defn tests-file [slug] (io/file (exercise-dir slug) "test" (tests-file-name slug))) diff --git a/generators/src/render.clj b/generators/src/render.clj new file mode 100644 index 000000000..ac4c0d623 --- /dev/null +++ b/generators/src/render.clj @@ -0,0 +1,18 @@ +(require + '[clojure.java.io :as io] + '[selmer.parser :refer [render render-file]]) + +(def data {:slug "isogram" + :cases (cases (canonical-data "isogram"))}) + +(def template "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.template") +(def tests "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.clj") + +(def toml-file "/home/erik/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.toml") + +(spit tests (render (slurp template) data)) +(def t (toml/read (io/reader toml-file))) +(set (map first (filter #(not= false (get (last %) "include")) t))) + +(.getAbsolutePath (io/file *file* ".." "..")) +(.getCanonicalPath (io/file *file* ".." ".." "..")) \ No newline at end of file From b0d636ef80bd1caa46a23ec4b13eb02339e5d0ec Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 11:27:51 +0100 Subject: [PATCH 05/26] Allow rendering --- .../collatz-conjecture/.meta/tests.clj | 60 ------------------- .../.meta/generator.template | 2 +- generators/src/canonical-data.clj | 20 +++---- generators/src/paths.clj | 2 +- generators/src/render.clj | 18 ------ generators/src/templates.clj | 14 +++++ 6 files changed, 25 insertions(+), 91 deletions(-) delete mode 100644 exercises/practice/collatz-conjecture/.meta/tests.clj rename exercises/practice/{collatz-conjecture => isogram}/.meta/generator.template (91%) delete mode 100644 generators/src/render.clj create mode 100644 generators/src/templates.clj diff --git a/exercises/practice/collatz-conjecture/.meta/tests.clj b/exercises/practice/collatz-conjecture/.meta/tests.clj deleted file mode 100644 index 2526ee896..000000000 --- a/exercises/practice/collatz-conjecture/.meta/tests.clj +++ /dev/null @@ -1,60 +0,0 @@ -(ns isogram-test - (:require [clojure.test :refer [deftest testing is]] - isogram)) - -(deftest isogram_test_1 - (testing "empty string") - (is (isogram/isogram? ""))) - -(deftest isogram_test_2 - (testing "isogram with only lower case characters") - (is (isogram/isogram? "isogram"))) - -(deftest isogram_test_3 - (testing "word with one duplicated character") - (is (not (isogram/isogram? "eleven")))) - -(deftest isogram_test_4 - (testing "word with one duplicated character from the end of the alphabet") - (is (not (isogram/isogram? "zzyzx")))) - -(deftest isogram_test_5 - (testing "longest reported english isogram") - (is (isogram/isogram? "subdermatoglyphic"))) - -(deftest isogram_test_6 - (testing "word with duplicated character in mixed case") - (is (not (isogram/isogram? "Alphabet")))) - -(deftest isogram_test_7 - (testing "word with duplicated character in mixed case, lowercase first") - (is (not (isogram/isogram? "alphAbet")))) - -(deftest isogram_test_8 - (testing "hypothetical isogrammic word with hyphen") - (is (isogram/isogram? "thumbscrew-japingly"))) - -(deftest isogram_test_9 - (testing "hypothetical word with duplicated character following hyphen") - (is (not (isogram/isogram? "thumbscrew-jappingly")))) - -(deftest isogram_test_10 - (testing "isogram with duplicated hyphen") - (is (isogram/isogram? "six-year-old"))) - -(deftest isogram_test_11 - (testing "made-up name that is an isogram") - (is (isogram/isogram? "Emily Jung Schwartzkopf"))) - -(deftest isogram_test_12 - (testing "duplicated character in the middle") - (is (not (isogram/isogram? "accentor")))) - -(deftest isogram_test_13 - (testing "same first and last characters") - (is (not (isogram/isogram? "angola")))) - -(deftest isogram_test_14 - (testing "word with duplicated character and with two hyphens") - (is (not (isogram/isogram? "up-to-date")))) - diff --git a/exercises/practice/collatz-conjecture/.meta/generator.template b/exercises/practice/isogram/.meta/generator.template similarity index 91% rename from exercises/practice/collatz-conjecture/.meta/generator.template rename to exercises/practice/isogram/.meta/generator.template index 7705b998f..b7378e4b6 100644 --- a/exercises/practice/collatz-conjecture/.meta/generator.template +++ b/exercises/practice/isogram/.meta/generator.template @@ -1,7 +1,7 @@ (ns isogram-test (:require [clojure.test :refer [deftest testing is]] isogram)) -{% for test_case in cases %} +{% for test_case in test_cases %} (deftest isogram_test_{{forloop.counter}} (testing "{{test_case.path|join:" - "}}") {% if test_case.expected %}(is (isogram/isogram? "{{test_case.input.phrase}}"))) {% else %}(is (not (isogram/isogram? "{{test_case.input.phrase}}")))) {% endif %} diff --git a/generators/src/canonical-data.clj b/generators/src/canonical-data.clj index 2a89001f6..1d8560edb 100644 --- a/generators/src/canonical-data.clj +++ b/generators/src/canonical-data.clj @@ -1,7 +1,6 @@ (ns canonical-data (:require [clojure.data.json :as json] [clojure.java.io :as io] - [clojure.java.shell :refer [sh]] [toml-clj.core :as toml] [clj-jgit.porcelain :refer [git-clone git-pull load-repo]] [log] @@ -9,25 +8,25 @@ (def git-url "https://github.com/exercism/problem-specifications.git") -(defn pull-repo [] +(defn- pull-repo [] (-> paths/prob-specs-dir (load-repo) (git-pull))) -(defn clone-repo [] (git-clone git-url :branch "main" :dir paths/prob-specs-dir)) +(defn- clone-repo [] (git-clone git-url :branch "main" :dir paths/prob-specs-dir)) -(defn sync-repo [] +(defn- sync-repo [] (try (pull-repo) (catch java.io.FileNotFoundException _ (clone-repo)))) -(defn canonical-data [slug] +(defn- canonical-data [slug] (let [file (paths/canonical-data-file slug)] (if (.exists file) (json/read (io/reader file) :key-fn keyword) (log/error (str "No canonical-data.json found for exercise '" slug "'"))))) -(defn excluded-uuids [slug] +(defn- excluded-uuids [slug] (let [file (paths/tests-toml-file slug)] (if (.exists file) (->> file @@ -38,16 +37,16 @@ (set)) (log/error (str "No tests.toml data found for exercise '" slug "'"))))) -(defn excluded? [slug] +(defn- excluded? [slug] (let [excluded (excluded-uuids slug)] (fn [node] (contains? excluded (:uuid node))))) -(defn node->test-case [node path] +(defn- node->test-case [node path] (-> node (assoc :path path :error (get-in node [:expected :error])) (dissoc :reimplements :comments :scenarios))) -(defn test-case-nodes +(defn- test-case-nodes ([node] (test-case-nodes node [])) ([node path] (let [description (:description node) @@ -64,5 +63,4 @@ (remove (excluded? slug)) (into []))) -(sync-prob-specs) -(test-cases "isogram") +(sync-repo) diff --git a/generators/src/paths.clj b/generators/src/paths.clj index ed3abc177..69b95baff 100644 --- a/generators/src/paths.clj +++ b/generators/src/paths.clj @@ -10,5 +10,5 @@ (defn canonical-data-file [slug] (io/file prob-specs-dir "exercises" slug "canonical-data.json")) (defn tests-toml-file [slug] (io/file (exercise-dir slug) ".meta" "tests.toml")) (defn generator-template-file [slug] (io/file (exercise-dir slug) ".meta" "generator.template")) -(defn tests-file-name [slug] (str "test_" (str/replace slug "-" "_") ".clj")) +(defn tests-file-name [slug] (str (str/replace slug "-" "_") "_test.clj")) (defn tests-file [slug] (io/file (exercise-dir slug) "test" (tests-file-name slug))) diff --git a/generators/src/render.clj b/generators/src/render.clj deleted file mode 100644 index ac4c0d623..000000000 --- a/generators/src/render.clj +++ /dev/null @@ -1,18 +0,0 @@ -(require - '[clojure.java.io :as io] - '[selmer.parser :refer [render render-file]]) - -(def data {:slug "isogram" - :cases (cases (canonical-data "isogram"))}) - -(def template "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.template") -(def tests "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.clj") - -(def toml-file "/home/erik/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.toml") - -(spit tests (render (slurp template) data)) -(def t (toml/read (io/reader toml-file))) -(set (map first (filter #(not= false (get (last %) "include")) t))) - -(.getAbsolutePath (io/file *file* ".." "..")) -(.getCanonicalPath (io/file *file* ".." ".." "..")) \ No newline at end of file diff --git a/generators/src/templates.clj b/generators/src/templates.clj new file mode 100644 index 000000000..a3f8a37b1 --- /dev/null +++ b/generators/src/templates.clj @@ -0,0 +1,14 @@ +(ns templates + (:require [selmer.parser :as selmer] + [log] + [paths])) + +(defn render [slug test-cases] + (let [data {:slug slug :test_cases test-cases} + template-file (paths/generator-template-file slug) + tests-file (paths/tests-file slug)] + (->> data + (selmer/render (slurp template-file)) + (spit tests-file)))) + +(templates/render "isogram" (canonical-data/test-cases "isogram")) \ No newline at end of file From 6d72b0d72fa04ebfd4124f46c3edebb5cffd7c17 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 11:35:12 +0100 Subject: [PATCH 06/26] More templates cleanup --- generators/src/generator.clj | 2 ++ generators/src/templates.clj | 20 ++++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/generators/src/generator.clj b/generators/src/generator.clj index 4b89bffbc..861647211 100644 --- a/generators/src/generator.clj +++ b/generators/src/generator.clj @@ -1,3 +1,5 @@ +(ns generator) + (require '[clojure.data.json :as json] '[clojure.java.shell :refer [sh]] diff --git a/generators/src/templates.clj b/generators/src/templates.clj index a3f8a37b1..878576e22 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -3,12 +3,16 @@ [log] [paths])) +(defn render-template [data template] + (selmer/render (slurp template) data)) + (defn render [slug test-cases] - (let [data {:slug slug :test_cases test-cases} - template-file (paths/generator-template-file slug) - tests-file (paths/tests-file slug)] - (->> data - (selmer/render (slurp template-file)) - (spit tests-file)))) - -(templates/render "isogram" (canonical-data/test-cases "isogram")) \ No newline at end of file + (let [data {:slug slug :test_cases test-cases}] + (render-template data (paths/generator-template-file slug)))) + +(defn generate-tests-file [slug test-cases] + (->> test-cases + (render slug) + (spit (paths/tests-file slug)))) + +(templates/generate-tests-file "isogram" (canonical-data/test-cases "isogram")) \ No newline at end of file From e31e8c5d1feccb3365d1d4cf929bdf87d9db8189 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 12:07:48 +0100 Subject: [PATCH 07/26] Allow running as main --- generators/deps.edn | 3 +- ...{canonical-data.clj => canonical_data.clj} | 3 +- generators/src/generator.clj | 102 ++++-------------- generators/src/log.clj | 3 + generators/src/paths.clj | 2 +- generators/src/templates.clj | 13 ++- 6 files changed, 34 insertions(+), 92 deletions(-) rename generators/src/{canonical-data.clj => canonical_data.clj} (98%) diff --git a/generators/deps.edn b/generators/deps.edn index 8a917fb88..c16be83d2 100644 --- a/generators/deps.edn +++ b/generators/deps.edn @@ -1,4 +1,5 @@ -{:deps {org.clojure/data.json {:mvn/version "2.5.1"} +{:paths ["src"] + :deps {org.clojure/data.json {:mvn/version "2.5.1"} selmer/selmer {:mvn/version "1.12.61"} io.github.tonsky/toml-clj {:mvn/version "0.1.0"} clj-jgit/clj-jgit {:mvn/version "1.1.0"}}} \ No newline at end of file diff --git a/generators/src/canonical-data.clj b/generators/src/canonical_data.clj similarity index 98% rename from generators/src/canonical-data.clj rename to generators/src/canonical_data.clj index 1d8560edb..87ee71e68 100644 --- a/generators/src/canonical-data.clj +++ b/generators/src/canonical_data.clj @@ -15,7 +15,7 @@ (defn- clone-repo [] (git-clone git-url :branch "main" :dir paths/prob-specs-dir)) -(defn- sync-repo [] +(defn sync-repo [] (try (pull-repo) (catch java.io.FileNotFoundException _ (clone-repo)))) @@ -63,4 +63,3 @@ (remove (excluded? slug)) (into []))) -(sync-repo) diff --git a/generators/src/generator.clj b/generators/src/generator.clj index 861647211..4c7b93c09 100644 --- a/generators/src/generator.clj +++ b/generators/src/generator.clj @@ -1,84 +1,18 @@ -(ns generator) - -(require - '[clojure.data.json :as json] - '[clojure.java.shell :refer [sh]] - '[clojure.java.io :as io] - '[selmer.parser :refer [render render-file]] - '[toml-clj.core :as toml]) - -(defn error [message] - (println message) - (System/exit 1)) - -(def root-dir (.getCanonicalPath (io/file *file* ".." ".." ".."))) -(def prob-specs-dir (io/file root-dir ".problem-specifications")) -(defn exercise-dir [slug] (io/file root-dir "exercises" "practice" slug)) - -(defn sync-prob-specs [] - (if (.isDirectory prob-specs-dir) - (sh "git" "pull" :dir prob-specs-dir) - (sh "git" "clone" "https://github.com/exercism/problem-specifications.git" prob-specs-dir))) - -(defn canonical-data-file [slug] (io/file prob-specs-dir "exercises" slug "canonical-data.json")) -(defn canonical-data [slug] - (let [file (canonical-data-file slug)] - (if (.exists file) - (json/read (io/reader file) :key-fn keyword) - (error (str "No canonical-data.json found for exercise '" slug "'"))))) - -(defn tests-toml-file [slug] (io/file (exercise-dir slug) ".meta" "tests.toml")) -(defn excluded-uuids [slug] - (let [file (tests-toml-file slug)] - (if (.exists file) - (->> file - (io/reader) - (toml/read) - (filter #(= false (get (last %) "include"))) - (map first) - (set)) - (error (str "No tests.toml data found for exercise '" slug "'"))))) - -(defn excluded? [slug] - (let [excluded (excluded-uuids slug)] - (fn [node] (contains? excluded (:uuid node))))) - -(defn node->test-case [node path] - (-> node - (assoc :path path :error (get-in node [:expected :error])) - (dissoc :reimplements :comments :scenarios))) - -(defn test-case-nodes - ([node] (test-case-nodes node [])) - ([node path] - (let [description (:description node) - children (:cases node) - updated-path (if description (conj path description) path)] - (if children - (mapcat #(test-case-nodes % updated-path) children) - [(node->test-case node updated-path)])))) - -(defn test-cases [slug] - (->> slug - (canonical-data) - (test-case-nodes) - (remove (excluded? slug)) - (into []))) - -(sync-prob-specs) -(test-cases "isogram") - -(def data {:slug "isogram" - :cases (cases (canonical-data "isogram"))}) - -(def template "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.template") -(def tests "/Users/erik/Code/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.clj") - -(def toml-file "/home/erik/exercism/tracks/clojure/exercises/practice/collatz-conjecture/.meta/tests.toml") - -(spit tests (render (slurp template) data)) -(def t (toml/read (io/reader toml-file))) -(set (map first (filter #(not= false (get (last %) "include")) t))) - -(.getAbsolutePath (io/file *file* ".." "..")) -(.getCanonicalPath (io/file *file* ".." ".." "..")) \ No newline at end of file +(ns generator + (:require [canonical-data] + [templates] + [log])) + +(defn- slugs-to-generate [args] + (let [slugs templates/exercises-with-template] + (if-let [slug (first args)] + (if (contains? slugs slug) + [slug] + (log/error (str "No template found for exercise '" slug "'"))) + slugs))) + +(defn- run [args] + (canonical-data/sync-repo) + (doseq [slug (slugs-to-generate args)] + (log/normal (str "Generating tests for exercise '" slug "'")) + (templates/generate-tests-file slug (canonical-data/test-cases slug)))) diff --git a/generators/src/log.clj b/generators/src/log.clj index b956a9a79..4f31d44d1 100644 --- a/generators/src/log.clj +++ b/generators/src/log.clj @@ -1,5 +1,8 @@ (ns log) +(defn normal [message] + (println message)) + (defn error [message] (println message) (System/exit 1)) diff --git a/generators/src/paths.clj b/generators/src/paths.clj index 69b95baff..f11e9b1dd 100644 --- a/generators/src/paths.clj +++ b/generators/src/paths.clj @@ -2,7 +2,7 @@ (:require [clojure.java.io :as io] [clojure.string :as str])) -(def generators-dir (.getCanonicalPath (io/file *file* ".." ".."))) +(def generators-dir (.getCanonicalPath (io/file "."))) (def root-dir (.getCanonicalPath (io/file generators-dir ".."))) (def prob-specs-dir (io/file generators-dir ".problem-specifications")) (def exercises-dir (io/file root-dir "exercises" "practice")) diff --git a/generators/src/templates.clj b/generators/src/templates.clj index 878576e22..2189daa3c 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -3,10 +3,17 @@ [log] [paths])) -(defn render-template [data template] +(def exercises-with-template + (->> paths/exercises-dir + (file-seq) + (filter #(.isFile %)) + (filter #(= "generator.template" (.getName %))) + (mapv #(-> % (.getParentFile) (.getParentFile) (.getName))))) + +(defn- render-template [data template] (selmer/render (slurp template) data)) -(defn render [slug test-cases] +(defn- render [slug test-cases] (let [data {:slug slug :test_cases test-cases}] (render-template data (paths/generator-template-file slug)))) @@ -14,5 +21,3 @@ (->> test-cases (render slug) (spit (paths/tests-file slug)))) - -(templates/generate-tests-file "isogram" (canonical-data/test-cases "isogram")) \ No newline at end of file From fbd0f8be07793d1254ef3e807fc27f67e0b600af Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 12:26:03 +0100 Subject: [PATCH 08/26] Main work --- bin/generate-tests | 33 +++++++++++++++++++++++++++++++++ generators/src/generator.clj | 15 ++++++++------- generators/src/templates.clj | 3 ++- 3 files changed, 43 insertions(+), 8 deletions(-) create mode 100755 bin/generate-tests diff --git a/bin/generate-tests b/bin/generate-tests new file mode 100755 index 000000000..4a8c346dc --- /dev/null +++ b/bin/generate-tests @@ -0,0 +1,33 @@ +#!/usr/bin/env bash + +# Synopsis: +# Generate the tests for each exercise with a generator template. + +# Example: generate tests for all exercises with a generator template +# bin/generate-tests + +# Example: generate tests for a single exercise with a generator template +# bin/generate-tests two-fer + +set -eo pipefail + +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." +} + +required_tool clj + +exercise_slug="${1}" + +pushd generators >/dev/null || die "Could not change to the 'generators' directory" + +if [ -z "${exercise_slug}" ]; then + clj -X generator/run +else + clj -X generator/run :exercise "${exercise_slug}" +fi + +popd >/dev/null diff --git a/generators/src/generator.clj b/generators/src/generator.clj index 4c7b93c09..7723cc2ad 100644 --- a/generators/src/generator.clj +++ b/generators/src/generator.clj @@ -1,18 +1,19 @@ (ns generator - (:require [canonical-data] + (:require [clojure.string :as str] + [canonical-data] [templates] [log])) -(defn- slugs-to-generate [args] +(defn- slugs-to-generate [slug] (let [slugs templates/exercises-with-template] - (if-let [slug (first args)] + (if (str/blank? slug) + slugs (if (contains? slugs slug) [slug] - (log/error (str "No template found for exercise '" slug "'"))) - slugs))) + (log/error (str "No template found for exercise '" slug "'")))))) -(defn- run [args] +(defn- run [{:keys [exercise]}] (canonical-data/sync-repo) - (doseq [slug (slugs-to-generate args)] + (doseq [slug (slugs-to-generate (str exercise))] (log/normal (str "Generating tests for exercise '" slug "'")) (templates/generate-tests-file slug (canonical-data/test-cases slug)))) diff --git a/generators/src/templates.clj b/generators/src/templates.clj index 2189daa3c..74d69ef89 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -8,7 +8,8 @@ (file-seq) (filter #(.isFile %)) (filter #(= "generator.template" (.getName %))) - (mapv #(-> % (.getParentFile) (.getParentFile) (.getName))))) + (map #(-> % (.getParentFile) (.getParentFile) (.getName))) + (set))) (defn- render-template [data template] (selmer/render (slurp template) data)) From 38bde28b2b7538f1abb5150fd470288f761cf711 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 14:42:03 +0100 Subject: [PATCH 09/26] Remove old generator --- _generators/clock_generator.clj | 5 - _generators/generator.clj | 72 ------------- _generators/list-ops-generator.clj | 167 ----------------------------- _generators/zipper-generator.clj | 139 ------------------------ 4 files changed, 383 deletions(-) delete mode 100644 _generators/clock_generator.clj delete mode 100644 _generators/generator.clj delete mode 100644 _generators/list-ops-generator.clj delete mode 100644 _generators/zipper-generator.clj diff --git a/_generators/clock_generator.clj b/_generators/clock_generator.clj deleted file mode 100644 index a627ab01e..000000000 --- a/_generators/clock_generator.clj +++ /dev/null @@ -1,5 +0,0 @@ -(ns clock-generator) - -(defn munge-data - [test-data] - test-data) diff --git a/_generators/generator.clj b/_generators/generator.clj deleted file mode 100644 index 8146d7b4a..000000000 --- a/_generators/generator.clj +++ /dev/null @@ -1,72 +0,0 @@ -(ns generator - (:require [cheshire.core :as json] - [clojure.java.shell :refer [sh]] - [clojure.java.io :refer [reader]] - [stencil.core :as stencil]) - (:import (java.io File IOException))) - -(defn file-exists? - "Helper function to determine if a given file exists." - [path] - (.exists (File. path))) - -(defn warn-and-exit - "Wrapper function to warn with the given message and then exit with a non-zero return" - [message] - (println message) - (System/exit 1)) - -(defn clone-test-data - "Clone the x-common repo from github" - [] - (sh "git" "clone" "git@github.com:exercism/x-common")) - -(defn load-test-data - "Clones and loads the test data for the given exercise" - [exercise-name] - (when-not (file-exists? "x-common") - (clone-test-data)) - (let [test-data-filename (format "x-common/exercises/%s/canonical-data.json" exercise-name)] - (when-not (file-exists? test-data-filename) - (warn-and-exit - (format "Could not find test data for %s (looking in %s)" - exercise-name test-data-filename))) - (json/parse-stream (reader test-data-filename)))) - -(defn munge-test-data - "Loads the generator namespace for the exercise and calls the munge-data function on the given test-data." - [exercise-name test-data] - (let [exercise-ns (symbol (str exercise-name "-generator"))] - (try - (require [exercise-ns]) - (if-let [munge-data-fn (ns-resolve exercise-ns (symbol "munge-data"))] - (munge-data-fn test-data) - (do - (println (format "No munge-data function defined in %s" exercise-ns)) - (println (format "Skipping any munging of canonical-data for %s" exercise-name)) - test-data)) - (catch IOException e - (println (format "Could not require %s due to an exception:\n\t%s" exercise-ns (.getMessage e))) - (println (format "Skipping any munging of canonical-data for %s" exercise-name)) - test-data)))) - -(defn generate-test-data - "Munges the test-data and renders the test for the exercise using the test template." - [exercise-name test-template-path test-data] - (let [munged-test-data (munge-test-data exercise-name test-data) - template (slurp test-template-path)] - (spit - (format "exercises/%s/test/%s_test.clj" exercise-name exercise-name) - (stencil/render-string template munged-test-data)))) - -(defn -main - "Uses the test template for the exercise and test data to generate test cases." - [exercise-name & args] - (let [test-template-path (format "exercises/%s/.meta/%s.mustache" exercise-name exercise-name) - test-data (load-test-data exercise-name)] - (if (file-exists? test-template-path) - (do - (generate-test-data exercise-name test-template-path test-data) - (println (format "Generated tests for %s exercise using template %s" exercise-name test-template-path))) - (warn-and-exit (format "No exercise test template found at '%s'" test-template-path)))) - (shutdown-agents)) diff --git a/_generators/list-ops-generator.clj b/_generators/list-ops-generator.clj deleted file mode 100644 index e3eb86b7c..000000000 --- a/_generators/list-ops-generator.clj +++ /dev/null @@ -1,167 +0,0 @@ -#!/usr/bin/env bb - -(require '[cheshire.core :as json] - '[babashka.fs :as fs] - '[clojure.string :as str] - '[clojure.edn :as edn]) - -(comment - (def slug "list-ops")) - -(def data - (let [url "https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/"] - {:canonical-data (json/parse-string (slurp (str url "/" slug "/canonical-data.json")) true) - :description (slurp (str url "/" slug "/description.md")) - :metadata (slurp (str url "/" slug "/metadata.toml"))})) - -(second - (str/split (:metadata data) #"=")) - -(defn get-meta - "Returns a vector containing the exercise title and blurb" - [data] - (mapv last - (map #(map str/trim (str/split % #"=")) - (str/split-lines (:metadata data))))) - -(defn init-deps! [data] - (fs/create-dirs (fs/path "exercises" "practice" - (:exercise (:canonical-data data)) "src")) - (spit (str (fs/file "exercises" "practice" - (:exercise (:canonical-data data)) - "deps.edn")) - "{:aliases {:test {:extra-paths [\"test\"] - :extra-deps {io.github.cognitect-labs/test-runner - {:git/url \"https://github.com/cognitect-labs/test-runner.git\" - :sha \"705ad25bbf0228b1c38d0244a36001c2987d7337\"}} - :main-opts [\"-m\" \"cognitect.test-runner\"] - :exec-fn cognitect.test-runner.api/test}}}")) - -(comment - (init-deps! data)) - -(defn init-lein! [data] - (let [slug (:exercise (:canonical-data data))] - (spit (str (fs/file "exercises" "practice" - (:exercise (:canonical-data data)) "project.clj")) - (str "(defproject " slug " \"0.1.0-SNAPSHOT\" - :description \"" slug " exercise.\" - :url \"https://github.com/exercism/clojure/tree/main/exercises/" slug "\" - :dependencies [[org.clojure/clojure \"1.10.0\"]]) -")))) - -(comment - (init-lein! data)) - -(defn test-ns-form [data] - (str "(ns " (:exercise data) "-test - (:require [clojure.test :refer [deftest testing is]]\n " - (:exercise data) "))\n\n")) - -(defn src-ns-form [data] - (str "(ns " (:exercise data) ")\n\n")) - -(defn trans-fn [s] - (let [[args body] (str/split s #"->") - arg-strs (mapv str (edn/read-string args)) - [arg1 op arg2] (str/split (str/trim body) #"\s")] - (str "(fn [" (apply str (interpose " " arg-strs)) "] " - "(" op " " arg1 " " arg2 "))"))) - -(comment - (trans-fn "(x) -> x + 1") - (trans-fn "(x, y) -> x * y") - (trans-fn "(acc, el) -> el * acc")) - -(defn testing-form [slug test-case] - (let [property (symbol (str slug "/" (:property test-case))) - input (:input test-case) - args (map #(get input %) (keys input))] - (str " (testing \"" (:description test-case) "\" - (is (= " (:expected test-case) " " - (reverse (into (list property) args)) ")))"))) - -(comment - (testing-form "list-ops" (first (:cases (first (:cases (:canonical-data data))))))) - -(defn testing-forms - "Outputs a sequence of the test cases for a given property name - given its name as a string and the canonical data." - [property data] - (let [test-cases (filter #(= property (:property %)) - (mapcat :cases - (:cases (:canonical-data data))))] - (map #(testing-form (:exercise (:canonical-data data)) %) test-cases))) - -(comment - (testing-forms "append" data)) - -(defn deftest-forms [data] - (for [property (distinct (map :property (mapcat :cases - (:cases (:canonical-data data)))))] - (str "(deftest " property "-test\n" - (apply str (interpose "\n" - (testing-forms property data))) - ")"))) - -(comment - (deftest-forms data)) - -(defn init-tests! [data] - (let [path (fs/path "exercises" "practice" - (:exercise (:canonical-data data)) "test")] - (when-not (fs/directory? path) - (fs/create-dir path)) - (spit (str (fs/file "exercises" "practice" - (:exercise (:canonical-data data)) "test" - (str (str/replace (:exercise (:canonical-data data)) "-" "_") - "_test.clj"))) - (str (test-ns-form (:canonical-data data)) - (apply str (interpose "\n\n" - (deftest-forms data))))))) - -(comment - (init-tests! data)) - -(defn init-src! [data] - (spit (str (fs/file "exercises" "practice" (:exercise (:canonical-data data)) "src" - (str (str/replace (:exercise (:canonical-data data)) - "-" "_") ".clj"))) - (str (src-ns-form (:canonical-data data)) - (apply str (interpose "\n\n" - (for [property (distinct (map :property (mapcat :cases - (:cases (:canonical-data data)))))] - (str "(defn " property " []\n )"))))))) - -(comment - (init-src! data)) - -(defn init-description! [data] - (let [path ["exercises" "practice" (:exercise (:canonical-data data)) ".docs"]] - (when-not (fs/directory? (apply fs/path path)) - (fs/create-dir (apply fs/path path)) - (spit (str (apply fs/file (conj path "instructions.md"))) - (:description data))))) - -(comment - (init-description! data)) - -(defn config [data author blurb] - (let [slug (:exercise (:canonical-data data))] - {:authors [author], - :contributors [], - :files {:solution [(str "src/" (str/replace slug "-" "_") ".clj")], - :test [(str "test/" (str/replace slug "-" "_") "_test.clj")], - :example [".meta/example.clj"]}, - :blurb blurb})) - -(defn init-config! [data] - (let [path ["exercises" "practice" (:exercise (:canonical-data data)) ".meta"]] - (when-not (fs/directory? (apply fs/path path)) - (fs/create-dirs (apply fs/path (conj path "src"))) - (spit (str (apply fs/file (conj path "config.json"))) - (json/generate-string (config data "porkostomus" (last (get-meta data))) - {:pretty true}))))) - -(comment - (init-config! data)) \ No newline at end of file diff --git a/_generators/zipper-generator.clj b/_generators/zipper-generator.clj deleted file mode 100644 index 042cb6fdc..000000000 --- a/_generators/zipper-generator.clj +++ /dev/null @@ -1,139 +0,0 @@ -#!/usr/bin/env bb - -(require '[cheshire.core :as json] - '[babashka.fs :as fs] - '[clojure.string :as str]) - -(comment - (def slug "zipper")) - -(def data - (let [url "https://raw.githubusercontent.com/exercism/problem-specifications/main/exercises/"] - {:canonical-data (json/parse-string (slurp (str url "/" slug "/canonical-data.json")) true) - :description (slurp (str url "/" slug "/description.md")) - :metadata (slurp (str url "/" slug "/metadata.toml"))})) - -(second - (str/split (:metadata data) #"=")) - -(defn get-meta - "Returns a vector containing the exercise title and blurb" - [data] - (mapv last - (map #(map str/trim (str/split % #"=")) - (str/split-lines (:metadata data))))) - -(defn init-deps [data] - (fs/create-dirs (fs/path "exercises" "practice" - (:exercise (:canonical-data data)) "src")) - (spit (str (fs/file "exercises" "practice" - (:exercise (:canonical-data data)) - "deps.edn")) - "{:aliases {:test {:extra-paths [\"test\"] - :extra-deps {io.github.cognitect-labs/test-runner - {:git/url \"https://github.com/cognitect-labs/test-runner.git\" - :sha \"705ad25bbf0228b1c38d0244a36001c2987d7337\"}} - :main-opts [\"-m\" \"cognitect.test-runner\"] - :exec-fn cognitect.test-runner.api/test}}}")) - -(defn init-lein [data] - (let [slug (:exercise (:canonical-data data))] - (spit (str (fs/file "exercises" "practice" - (:exercise (:canonical-data data)) "project.clj")) - (str "(defproject " slug " \"0.1.0-SNAPSHOT\" - :description \"" slug " exercise.\" - :url \"https://github.com/exercism/clojure/tree/main/exercises/practice/" slug "\" - :dependencies [[org.clojure/clojure \"1.10.0\"]]) -")))) - -(defn test-ns-form [data] - (str "(ns " (:exercise data) "-test - (:require [clojure.test :refer [deftest testing is]]\n " - (:exercise data) "))\n\n")) - -(defn src-ns-form [data] - (str "(ns " (:exercise data) ")\n\n")) - -(defn testing-form [slug test-case] - (let [property (symbol (str slug "/" (:property test-case))) - input (:input test-case) - args (map #(get input %) (keys input))] - (str " (testing \"" (:description test-case) "\" - (is (= " (:expected test-case) " " - (reverse (into (list property) args)) ")))"))) - -(defn zipper-generator [slug test-case] - (let [input (:input test-case) - ops (for [op (:operations input)] - (if (contains? op :item) - (str "(zipper/" (:operation op) " " - (if (nil? (:item op)) - "nil" - (str (:item op))) ")") - (str "zipper/" (:operation op))))] - (str " (testing \"" (:description test-case) "\" - (is (= " (if (nil? (:value (:expected test-case))) - "nil" (:value (:expected test-case))) " " - "\n (-> " (:initialTree input) "\n " - (apply str (interpose "\n " ops)) "))))"))) - -(defn testing-forms - "Outputs a sequence of the test cases for a given property name - given its name as a string and the canonical data." - [property data] - (let [test-cases (filter #(= property (:property %)) (:cases data))] - (map #(zipper-generator (:exercise data) %) test-cases))) - -(defn deftest-forms [data] - (for [property (distinct (map :property (:cases (:canonical-data data))))] - (str "(deftest " property "-test\n" - (apply str (interpose "\n" - (testing-forms property (:canonical-data data)))) - ")"))) - -(defn init-tests [data] - #_(fs/create-dir (fs/path "exercises" "practice" - (:exercise (:canonical-data data)) "test")) - (spit (str (fs/file "exercises" "practice" - (:exercise (:canonical-data data)) "test" - (str (str/replace (:exercise (:canonical-data data)) "-" "_") - "_test.clj"))) - (str (test-ns-form (:canonical-data data)) - (apply str (interpose "\n\n" - (deftest-forms data)))))) - -(defn init-src [data] - (spit (str (fs/file "exercises" "practice" (:exercise (:canonical-data data)) "src" - (str (str/replace (:exercise (:canonical-data data)) - "-" "_") ".clj"))) - (str (src-ns-form (:canonical-data data)) - (apply str (interpose "\n\n" - (for [property (distinct (map :property (:cases (:canonical-data data))))] - (str "(defn " property " []\n )"))))))) - -(defn init-description! [data] - (let [path ["exercises" "practice" (:exercise (:canonical-data data)) ".docs"]] - (when-not (fs/directory? (apply fs/path path)) - (fs/create-dir (apply fs/path path)) - (spit (str (apply fs/file (conj path "instructions.md"))) - (:description data))))) - -(defn config [data author blurb] - (let [slug (:exercise (:canonical-data data))] - {:authors [author], - :contributors [], - :files {:solution [(str "src/" (str/replace slug "-" "_") ".clj")], - :test [(str "test/" (str/replace slug "-" "_") "_test.clj")], - :example [".meta/example.clj"]}, - :blurb blurb})) - -(defn init-config! [data] - (let [path ["exercises" "practice" (:exercise (:canonical-data data)) ".meta"]] - (when-not (fs/directory? (apply fs/path path)) - (fs/create-dirs (apply fs/path (conj path "src"))) - (spit (str (apply fs/file (conj path "config.json"))) - (json/generate-string (config data "porkostomus" (last (get-meta data))) - {:pretty true}))))) - -(comment - (init-config! data)) \ No newline at end of file From e5592fc1836c121bf6a266c09324de3c3977b0f4 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 14:56:05 +0100 Subject: [PATCH 10/26] Add two-fer template --- .../practice/two-fer/.meta/generator.template | 8 ++++++++ .../practice/two-fer/test/two_fer_test.clj | 20 +++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) create mode 100644 exercises/practice/two-fer/.meta/generator.template diff --git a/exercises/practice/two-fer/.meta/generator.template b/exercises/practice/two-fer/.meta/generator.template new file mode 100644 index 000000000..daf1636dd --- /dev/null +++ b/exercises/practice/two-fer/.meta/generator.template @@ -0,0 +1,8 @@ +(ns two-fer-test + (:require [clojure.test :refer [deftest testing is]] + two-fer)) +{% for test_case in test_cases %} +(deftest two-fer_test_{{forloop.counter}} + (testing "{{test_case.path|join:" - "}}" + (is (= "{{test_case.expected}}" (two-fer/two-fer{% if test_case.input.name %} "{{test_case.input.name}}"{% endif %}))))) +{% endfor %} diff --git a/exercises/practice/two-fer/test/two_fer_test.clj b/exercises/practice/two-fer/test/two_fer_test.clj index a1ac32968..435c0e824 100644 --- a/exercises/practice/two-fer/test/two_fer_test.clj +++ b/exercises/practice/two-fer/test/two_fer_test.clj @@ -1,12 +1,16 @@ (ns two-fer-test - (:require [clojure.test :refer [deftest is]] - two-fer)) + (:require [clojure.test :refer [deftest testing is]] + two-fer)) -(deftest two-fer-test - (is (= "One for you, one for me." (two-fer/two-fer)))) +(deftest two-fer_test_1 + (testing "no name given" + (is (= "One for you, one for me." (two-fer/two-fer))))) -(deftest name-alice-test - (is (= "One for Alice, one for me." (two-fer/two-fer "Alice")))) +(deftest two-fer_test_2 + (testing "a name given" + (is (= "One for Alice, one for me." (two-fer/two-fer "Alice"))))) + +(deftest two-fer_test_3 + (testing "another name given" + (is (= "One for Bob, one for me." (two-fer/two-fer "Bob"))))) -(deftest name-bob-test - (is (= "One for Bob, one for me." (two-fer/two-fer "Bob")))) From e1ad7aa78de203e6bc45d117e5377091d8ed9d25 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 14:56:12 +0100 Subject: [PATCH 11/26] Add isogram template --- .../practice/isogram/.meta/generator.template | 6 +- .../practice/isogram/test/isogram_test.clj | 75 +++++++++++++++---- .../practice/two-fer/.meta/generator.template | 2 +- .../practice/two-fer/test/two_fer_test.clj | 2 +- 4 files changed, 64 insertions(+), 21 deletions(-) diff --git a/exercises/practice/isogram/.meta/generator.template b/exercises/practice/isogram/.meta/generator.template index b7378e4b6..1e468c2f3 100644 --- a/exercises/practice/isogram/.meta/generator.template +++ b/exercises/practice/isogram/.meta/generator.template @@ -1,8 +1,8 @@ (ns isogram-test - (:require [clojure.test :refer [deftest testing is]] + (:require [clojure.test :refer [deftest testing is]] isogram)) {% for test_case in test_cases %} (deftest isogram_test_{{forloop.counter}} - (testing "{{test_case.path|join:" - "}}") - {% if test_case.expected %}(is (isogram/isogram? "{{test_case.input.phrase}}"))) {% else %}(is (not (isogram/isogram? "{{test_case.input.phrase}}")))) {% endif %} + (testing "{{test_case.path|join:" - "}}" + {% if test_case.expected %}(is (isogram/isogram? "{{test_case.input.phrase}}")))) {% else %}(is (not (isogram/isogram? "{{test_case.input.phrase}}")))){% endif %} {% endfor %} diff --git a/exercises/practice/isogram/test/isogram_test.clj b/exercises/practice/isogram/test/isogram_test.clj index 5c2cf69a8..227004d77 100644 --- a/exercises/practice/isogram/test/isogram_test.clj +++ b/exercises/practice/isogram/test/isogram_test.clj @@ -1,17 +1,60 @@ (ns isogram-test - (:require [clojure.test :refer [deftest is]] - isogram)) - -(deftest test-isograms - (is (isogram/isogram? "duplicates")) - (is (isogram/isogram? "subdermatoglyphic")) - (is (isogram/isogram? "thumbscrew-japingly")) - (is (isogram/isogram? "Hjelmqvist-Gryb-Zock-Pfund-Wax")) - (is (isogram/isogram? "Heizölrückstoßabdämpfung")) - (is (isogram/isogram? "Emily Jung Schwartzkopf"))) - -(deftest test-non-isograms - (is (not (isogram/isogram? "eleven"))) - (is (not (isogram/isogram? "Alphabet"))) - (is (not (isogram/isogram? "the quick brown fox"))) - (is (not (isogram/isogram? "éléphant")))) + (:require [clojure.test :refer [deftest testing is]] + isogram)) + +(deftest isogram_test_1 + (testing "empty string" + (is (isogram/isogram? "")))) + +(deftest isogram_test_2 + (testing "isogram with only lower case characters" + (is (isogram/isogram? "isogram")))) + +(deftest isogram_test_3 + (testing "word with one duplicated character" + (is (not (isogram/isogram? "eleven")))) + +(deftest isogram_test_4 + (testing "word with one duplicated character from the end of the alphabet" + (is (not (isogram/isogram? "zzyzx")))) + +(deftest isogram_test_5 + (testing "longest reported english isogram" + (is (isogram/isogram? "subdermatoglyphic")))) + +(deftest isogram_test_6 + (testing "word with duplicated character in mixed case" + (is (not (isogram/isogram? "Alphabet")))) + +(deftest isogram_test_7 + (testing "word with duplicated character in mixed case, lowercase first" + (is (not (isogram/isogram? "alphAbet")))) + +(deftest isogram_test_8 + (testing "hypothetical isogrammic word with hyphen" + (is (isogram/isogram? "thumbscrew-japingly")))) + +(deftest isogram_test_9 + (testing "hypothetical word with duplicated character following hyphen" + (is (not (isogram/isogram? "thumbscrew-jappingly")))) + +(deftest isogram_test_10 + (testing "isogram with duplicated hyphen" + (is (isogram/isogram? "six-year-old")))) + +(deftest isogram_test_11 + (testing "made-up name that is an isogram" + (is (isogram/isogram? "Emily Jung Schwartzkopf")))) + +(deftest isogram_test_12 + (testing "duplicated character in the middle" + (is (not (isogram/isogram? "accentor")))) + +(deftest isogram_test_13 + (testing "same first and last characters" + (is (not (isogram/isogram? "angola")))) + +(deftest isogram_test_14 + (testing "word with duplicated character and with two hyphens" + (is (not (isogram/isogram? "up-to-date")))) + diff --git a/exercises/practice/two-fer/.meta/generator.template b/exercises/practice/two-fer/.meta/generator.template index daf1636dd..673a3d0d2 100644 --- a/exercises/practice/two-fer/.meta/generator.template +++ b/exercises/practice/two-fer/.meta/generator.template @@ -1,5 +1,5 @@ (ns two-fer-test - (:require [clojure.test :refer [deftest testing is]] + (:require [clojure.test :refer [deftest testing is]] two-fer)) {% for test_case in test_cases %} (deftest two-fer_test_{{forloop.counter}} diff --git a/exercises/practice/two-fer/test/two_fer_test.clj b/exercises/practice/two-fer/test/two_fer_test.clj index 435c0e824..6be75ad22 100644 --- a/exercises/practice/two-fer/test/two_fer_test.clj +++ b/exercises/practice/two-fer/test/two_fer_test.clj @@ -1,5 +1,5 @@ (ns two-fer-test - (:require [clojure.test :refer [deftest testing is]] + (:require [clojure.test :refer [deftest testing is]] two-fer)) (deftest two-fer_test_1 From e27d9adf3f68b480bf436f512ea7df6fe4837de6 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 15:31:49 +0100 Subject: [PATCH 12/26] Change library for templates --- exercises/practice/isogram/.meta/example.clj | 3 +- .../practice/isogram/.meta/generator.template | 17 +++++++---- .../practice/isogram/test/isogram_test.clj | 28 +++++++++---------- .../practice/two-fer/.meta/generator.template | 12 ++++---- generators/deps.edn | 4 +-- generators/src/canonical_data.clj | 13 +++++---- generators/src/templates.clj | 17 ++++------- 7 files changed, 51 insertions(+), 43 deletions(-) diff --git a/exercises/practice/isogram/.meta/example.clj b/exercises/practice/isogram/.meta/example.clj index ce0b8c146..9afdda6f1 100644 --- a/exercises/practice/isogram/.meta/example.clj +++ b/exercises/practice/isogram/.meta/example.clj @@ -2,4 +2,5 @@ (:require [clojure.string :as str])) (defn isogram? [word] - (apply distinct? (filter #(Character/isLetter %) (str/lower-case word)))) + (let [letters (filter #(Character/isLetter %) (str/lower-case word))] + (or (empty? letters) (apply distinct? letters)))) diff --git a/exercises/practice/isogram/.meta/generator.template b/exercises/practice/isogram/.meta/generator.template index 1e468c2f3..d49c8d19d 100644 --- a/exercises/practice/isogram/.meta/generator.template +++ b/exercises/practice/isogram/.meta/generator.template @@ -1,8 +1,15 @@ (ns isogram-test (:require [clojure.test :refer [deftest testing is]] isogram)) -{% for test_case in test_cases %} -(deftest isogram_test_{{forloop.counter}} - (testing "{{test_case.path|join:" - "}}" - {% if test_case.expected %}(is (isogram/isogram? "{{test_case.input.phrase}}")))) {% else %}(is (not (isogram/isogram? "{{test_case.input.phrase}}")))){% endif %} -{% endfor %} + +{{#test_cases}} +(deftest isogram_test_{{idx}} + (testing "{{description}}" + {{#expected}} + (is (isogram/isogram? "{{input.phrase}}")))) + {{/expected}} + {{^expected}} + (is (not (isogram/isogram? "{{input.phrase}}"))))) + {{/expected}} + +{{/test_cases}} diff --git a/exercises/practice/isogram/test/isogram_test.clj b/exercises/practice/isogram/test/isogram_test.clj index 227004d77..c727488bc 100644 --- a/exercises/practice/isogram/test/isogram_test.clj +++ b/exercises/practice/isogram/test/isogram_test.clj @@ -4,57 +4,57 @@ (deftest isogram_test_1 (testing "empty string" - (is (isogram/isogram? "")))) + (is (isogram/isogram? "")))) (deftest isogram_test_2 (testing "isogram with only lower case characters" - (is (isogram/isogram? "isogram")))) + (is (isogram/isogram? "isogram")))) (deftest isogram_test_3 (testing "word with one duplicated character" - (is (not (isogram/isogram? "eleven")))) + (is (not (isogram/isogram? "eleven"))))) (deftest isogram_test_4 (testing "word with one duplicated character from the end of the alphabet" - (is (not (isogram/isogram? "zzyzx")))) + (is (not (isogram/isogram? "zzyzx"))))) (deftest isogram_test_5 (testing "longest reported english isogram" - (is (isogram/isogram? "subdermatoglyphic")))) + (is (isogram/isogram? "subdermatoglyphic")))) (deftest isogram_test_6 (testing "word with duplicated character in mixed case" - (is (not (isogram/isogram? "Alphabet")))) + (is (not (isogram/isogram? "Alphabet"))))) (deftest isogram_test_7 (testing "word with duplicated character in mixed case, lowercase first" - (is (not (isogram/isogram? "alphAbet")))) + (is (not (isogram/isogram? "alphAbet"))))) (deftest isogram_test_8 (testing "hypothetical isogrammic word with hyphen" - (is (isogram/isogram? "thumbscrew-japingly")))) + (is (isogram/isogram? "thumbscrew-japingly")))) (deftest isogram_test_9 (testing "hypothetical word with duplicated character following hyphen" - (is (not (isogram/isogram? "thumbscrew-jappingly")))) + (is (not (isogram/isogram? "thumbscrew-jappingly"))))) (deftest isogram_test_10 (testing "isogram with duplicated hyphen" - (is (isogram/isogram? "six-year-old")))) + (is (isogram/isogram? "six-year-old")))) (deftest isogram_test_11 (testing "made-up name that is an isogram" - (is (isogram/isogram? "Emily Jung Schwartzkopf")))) + (is (isogram/isogram? "Emily Jung Schwartzkopf")))) (deftest isogram_test_12 (testing "duplicated character in the middle" - (is (not (isogram/isogram? "accentor")))) + (is (not (isogram/isogram? "accentor"))))) (deftest isogram_test_13 (testing "same first and last characters" - (is (not (isogram/isogram? "angola")))) + (is (not (isogram/isogram? "angola"))))) (deftest isogram_test_14 (testing "word with duplicated character and with two hyphens" - (is (not (isogram/isogram? "up-to-date")))) + (is (not (isogram/isogram? "up-to-date"))))) diff --git a/exercises/practice/two-fer/.meta/generator.template b/exercises/practice/two-fer/.meta/generator.template index 673a3d0d2..2497b0846 100644 --- a/exercises/practice/two-fer/.meta/generator.template +++ b/exercises/practice/two-fer/.meta/generator.template @@ -1,8 +1,10 @@ (ns two-fer-test (:require [clojure.test :refer [deftest testing is]] two-fer)) -{% for test_case in test_cases %} -(deftest two-fer_test_{{forloop.counter}} - (testing "{{test_case.path|join:" - "}}" - (is (= "{{test_case.expected}}" (two-fer/two-fer{% if test_case.input.name %} "{{test_case.input.name}}"{% endif %}))))) -{% endfor %} + +{{#test_cases}} +(deftest two-fer_test_{{idx}} + (testing "{{description}}" + (is (= "{{expected}}" (two-fer/two-fer{{#input.name}} "{{input.name}}"{{/input.name}}))))) + +{{/test_cases}} diff --git a/generators/deps.edn b/generators/deps.edn index c16be83d2..0e9843006 100644 --- a/generators/deps.edn +++ b/generators/deps.edn @@ -1,5 +1,5 @@ {:paths ["src"] :deps {org.clojure/data.json {:mvn/version "2.5.1"} - selmer/selmer {:mvn/version "1.12.61"} + pogonos/pogonos {:mvn/version "0.2.1"} io.github.tonsky/toml-clj {:mvn/version "0.1.0"} - clj-jgit/clj-jgit {:mvn/version "1.1.0"}}} \ No newline at end of file + clj-jgit/clj-jgit {:mvn/version "1.1.0"}}} diff --git a/generators/src/canonical_data.clj b/generators/src/canonical_data.clj index 87ee71e68..f5d74f822 100644 --- a/generators/src/canonical_data.clj +++ b/generators/src/canonical_data.clj @@ -4,7 +4,8 @@ [toml-clj.core :as toml] [clj-jgit.porcelain :refer [git-clone git-pull load-repo]] [log] - [paths])) + [paths] + [clojure.string :as str])) (def git-url "https://github.com/exercism/problem-specifications.git") @@ -41,9 +42,11 @@ (let [excluded (excluded-uuids slug)] (fn [node] (contains? excluded (:uuid node))))) -(defn- node->test-case [node path] +(defn- node->test-case [idx node] (-> node - (assoc :path path :error (get-in node [:expected :error])) + (assoc :idx (inc idx) + :description (str/join " - " (:path node)) + :error (get-in node [:expected :error])) (dissoc :reimplements :comments :scenarios))) (defn- test-case-nodes @@ -54,12 +57,12 @@ updated-path (if description (conj path description) path)] (if children (mapcat #(test-case-nodes % updated-path) children) - [(node->test-case node updated-path)])))) + [(assoc node :path updated-path)])))) (defn test-cases [slug] (->> slug (canonical-data) (test-case-nodes) (remove (excluded? slug)) + (map-indexed node->test-case) (into []))) - diff --git a/generators/src/templates.clj b/generators/src/templates.clj index 74d69ef89..59221748e 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -1,5 +1,6 @@ (ns templates - (:require [selmer.parser :as selmer] + (:require [pogonos.core :as pg] + [pogonos.output :as output] [log] [paths])) @@ -11,14 +12,8 @@ (map #(-> % (.getParentFile) (.getParentFile) (.getName))) (set))) -(defn- render-template [data template] - (selmer/render (slurp template) data)) - -(defn- render [slug test-cases] - (let [data {:slug slug :test_cases test-cases}] - (render-template data (paths/generator-template-file slug)))) - (defn generate-tests-file [slug test-cases] - (->> test-cases - (render slug) - (spit (paths/tests-file slug)))) + (pg/render-file + (paths/generator-template-file slug) + {:test_cases test-cases} + {:output (output/to-file (paths/tests-file slug))})) From 2e433ccfe89b71e5a67b392d5419968a7b0d2082 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 16:03:11 +0100 Subject: [PATCH 13/26] Add roman-numerals template --- .../roman-numerals/.meta/generator.template | 10 ++ .../test/roman_numerals_test.clj | 131 +++++++++++++----- 2 files changed, 103 insertions(+), 38 deletions(-) create mode 100644 exercises/practice/roman-numerals/.meta/generator.template diff --git a/exercises/practice/roman-numerals/.meta/generator.template b/exercises/practice/roman-numerals/.meta/generator.template new file mode 100644 index 000000000..51b91a397 --- /dev/null +++ b/exercises/practice/roman-numerals/.meta/generator.template @@ -0,0 +1,10 @@ +(ns roman-numerals-test + (:require [clojure.test :refer [deftest testing is]] + roman-numerals)) + +{{#test_cases}} +(deftest roman-numerals_test_{{idx}} + (testing "{{description}}" + (is (= "{{expected}}" (roman-numerals/numerals {{input.number}}))))) + +{{/test_cases}} diff --git a/exercises/practice/roman-numerals/test/roman_numerals_test.clj b/exercises/practice/roman-numerals/test/roman_numerals_test.clj index 967f02a3f..47c345043 100644 --- a/exercises/practice/roman-numerals/test/roman_numerals_test.clj +++ b/exercises/practice/roman-numerals/test/roman_numerals_test.clj @@ -1,57 +1,112 @@ (ns roman-numerals-test - (:require [clojure.test :refer [deftest is]] - roman-numerals)) + (:require [clojure.test :refer [deftest testing is]] + roman-numerals)) -(deftest one - (is (= "I" (roman-numerals/numerals 1)))) +(deftest roman-numerals_test_1 + (testing "1 is I" + (is (= "I" (roman-numerals/numerals 1))))) -(deftest two - (is (= "II" (roman-numerals/numerals 2)))) +(deftest roman-numerals_test_2 + (testing "2 is II" + (is (= "II" (roman-numerals/numerals 2))))) -(deftest three - (is (= "III" (roman-numerals/numerals 3)))) +(deftest roman-numerals_test_3 + (testing "3 is III" + (is (= "III" (roman-numerals/numerals 3))))) -(deftest four - (is (= "IV" (roman-numerals/numerals 4)))) +(deftest roman-numerals_test_4 + (testing "4 is IV" + (is (= "IV" (roman-numerals/numerals 4))))) -(deftest five - (is (= "V" (roman-numerals/numerals 5)))) +(deftest roman-numerals_test_5 + (testing "5 is V" + (is (= "V" (roman-numerals/numerals 5))))) -(deftest six - (is (= "VI" (roman-numerals/numerals 6)))) +(deftest roman-numerals_test_6 + (testing "6 is VI" + (is (= "VI" (roman-numerals/numerals 6))))) -(deftest nine - (is (= "IX" (roman-numerals/numerals 9)))) +(deftest roman-numerals_test_7 + (testing "9 is IX" + (is (= "IX" (roman-numerals/numerals 9))))) -(deftest twenty-seven - (is (= "XXVII" (roman-numerals/numerals 27)))) +(deftest roman-numerals_test_8 + (testing "16 is XVI" + (is (= "XVI" (roman-numerals/numerals 16))))) -(deftest forty-eight - (is (= "XLVIII" (roman-numerals/numerals 48)))) +(deftest roman-numerals_test_9 + (testing "27 is XXVII" + (is (= "XXVII" (roman-numerals/numerals 27))))) -(deftest fifty-nine - (is (= "LIX" (roman-numerals/numerals 59)))) +(deftest roman-numerals_test_10 + (testing "48 is XLVIII" + (is (= "XLVIII" (roman-numerals/numerals 48))))) -(deftest ninety-three - (is (= "XCIII" (roman-numerals/numerals 93)))) +(deftest roman-numerals_test_11 + (testing "49 is XLIX" + (is (= "XLIX" (roman-numerals/numerals 49))))) -(deftest one-hundred-forty-one - (is (= "CXLI" (roman-numerals/numerals 141)))) +(deftest roman-numerals_test_12 + (testing "59 is LIX" + (is (= "LIX" (roman-numerals/numerals 59))))) -(deftest one-hundred-sixty-three - (is (= "CLXIII" (roman-numerals/numerals 163)))) +(deftest roman-numerals_test_13 + (testing "66 is LXVI" + (is (= "LXVI" (roman-numerals/numerals 66))))) -(deftest four-hundred-two - (is (= "CDII" (roman-numerals/numerals 402)))) +(deftest roman-numerals_test_14 + (testing "93 is XCIII" + (is (= "XCIII" (roman-numerals/numerals 93))))) -(deftest five-hundred-seventy-five - (is (= "DLXXV" (roman-numerals/numerals 575)))) +(deftest roman-numerals_test_15 + (testing "141 is CXLI" + (is (= "CXLI" (roman-numerals/numerals 141))))) -(deftest nine-hundred-eleven - (is (= "CMXI" (roman-numerals/numerals 911)))) +(deftest roman-numerals_test_16 + (testing "163 is CLXIII" + (is (= "CLXIII" (roman-numerals/numerals 163))))) -(deftest one-thousand-twenty-four - (is (= "MXXIV" (roman-numerals/numerals 1024)))) +(deftest roman-numerals_test_17 + (testing "166 is CLXVI" + (is (= "CLXVI" (roman-numerals/numerals 166))))) + +(deftest roman-numerals_test_18 + (testing "402 is CDII" + (is (= "CDII" (roman-numerals/numerals 402))))) + +(deftest roman-numerals_test_19 + (testing "575 is DLXXV" + (is (= "DLXXV" (roman-numerals/numerals 575))))) + +(deftest roman-numerals_test_20 + (testing "666 is DCLXVI" + (is (= "DCLXVI" (roman-numerals/numerals 666))))) + +(deftest roman-numerals_test_21 + (testing "911 is CMXI" + (is (= "CMXI" (roman-numerals/numerals 911))))) + +(deftest roman-numerals_test_22 + (testing "1024 is MXXIV" + (is (= "MXXIV" (roman-numerals/numerals 1024))))) + +(deftest roman-numerals_test_23 + (testing "1666 is MDCLXVI" + (is (= "MDCLXVI" (roman-numerals/numerals 1666))))) + +(deftest roman-numerals_test_24 + (testing "3000 is MMM" + (is (= "MMM" (roman-numerals/numerals 3000))))) + +(deftest roman-numerals_test_25 + (testing "3001 is MMMI" + (is (= "MMMI" (roman-numerals/numerals 3001))))) + +(deftest roman-numerals_test_26 + (testing "3888 is MMMDCCCLXXXVIII" + (is (= "MMMDCCCLXXXVIII" (roman-numerals/numerals 3888))))) + +(deftest roman-numerals_test_27 + (testing "3999 is MMMCMXCIX" + (is (= "MMMCMXCIX" (roman-numerals/numerals 3999))))) -(deftest three-thousand - (is (= "MMM" (roman-numerals/numerals 3000)))) From a5628e7f56a1ddf701c18f7e83ebcf3f126a5328 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Fri, 10 Jan 2025 18:59:32 +0100 Subject: [PATCH 14/26] Move to handlebars --- .../practice/isogram/.meta/generator.template | 8 +++--- .../practice/isogram/test/isogram_test.clj | 27 ++++++++++--------- .../roman-numerals/.meta/generator.template | 2 -- .../practice/two-fer/.meta/generator.template | 2 -- generators/deps.edn | 2 +- generators/src/templates.clj | 11 ++++---- 6 files changed, 23 insertions(+), 29 deletions(-) diff --git a/exercises/practice/isogram/.meta/generator.template b/exercises/practice/isogram/.meta/generator.template index d49c8d19d..cec89491d 100644 --- a/exercises/practice/isogram/.meta/generator.template +++ b/exercises/practice/isogram/.meta/generator.template @@ -2,14 +2,12 @@ (:require [clojure.test :refer [deftest testing is]] isogram)) -{{#test_cases}} +{{#test_cases~}} (deftest isogram_test_{{idx}} (testing "{{description}}" - {{#expected}} + {{#expected~}} (is (isogram/isogram? "{{input.phrase}}")))) - {{/expected}} - {{^expected}} + {{else~}} (is (not (isogram/isogram? "{{input.phrase}}"))))) {{/expected}} - {{/test_cases}} diff --git a/exercises/practice/isogram/test/isogram_test.clj b/exercises/practice/isogram/test/isogram_test.clj index c727488bc..7b4cbcd7c 100644 --- a/exercises/practice/isogram/test/isogram_test.clj +++ b/exercises/practice/isogram/test/isogram_test.clj @@ -5,56 +5,57 @@ (deftest isogram_test_1 (testing "empty string" (is (isogram/isogram? "")))) - + (deftest isogram_test_2 (testing "isogram with only lower case characters" (is (isogram/isogram? "isogram")))) - + (deftest isogram_test_3 (testing "word with one duplicated character" (is (not (isogram/isogram? "eleven"))))) - + (deftest isogram_test_4 (testing "word with one duplicated character from the end of the alphabet" (is (not (isogram/isogram? "zzyzx"))))) - + (deftest isogram_test_5 (testing "longest reported english isogram" (is (isogram/isogram? "subdermatoglyphic")))) - + (deftest isogram_test_6 (testing "word with duplicated character in mixed case" (is (not (isogram/isogram? "Alphabet"))))) - + (deftest isogram_test_7 (testing "word with duplicated character in mixed case, lowercase first" (is (not (isogram/isogram? "alphAbet"))))) - + (deftest isogram_test_8 (testing "hypothetical isogrammic word with hyphen" (is (isogram/isogram? "thumbscrew-japingly")))) - + (deftest isogram_test_9 (testing "hypothetical word with duplicated character following hyphen" (is (not (isogram/isogram? "thumbscrew-jappingly"))))) - + (deftest isogram_test_10 (testing "isogram with duplicated hyphen" (is (isogram/isogram? "six-year-old")))) - + (deftest isogram_test_11 (testing "made-up name that is an isogram" (is (isogram/isogram? "Emily Jung Schwartzkopf")))) - + (deftest isogram_test_12 (testing "duplicated character in the middle" (is (not (isogram/isogram? "accentor"))))) - + (deftest isogram_test_13 (testing "same first and last characters" (is (not (isogram/isogram? "angola"))))) - + (deftest isogram_test_14 (testing "word with duplicated character and with two hyphens" (is (not (isogram/isogram? "up-to-date"))))) + diff --git a/exercises/practice/roman-numerals/.meta/generator.template b/exercises/practice/roman-numerals/.meta/generator.template index 51b91a397..94c3a71db 100644 --- a/exercises/practice/roman-numerals/.meta/generator.template +++ b/exercises/practice/roman-numerals/.meta/generator.template @@ -1,10 +1,8 @@ (ns roman-numerals-test (:require [clojure.test :refer [deftest testing is]] roman-numerals)) - {{#test_cases}} (deftest roman-numerals_test_{{idx}} (testing "{{description}}" (is (= "{{expected}}" (roman-numerals/numerals {{input.number}}))))) - {{/test_cases}} diff --git a/exercises/practice/two-fer/.meta/generator.template b/exercises/practice/two-fer/.meta/generator.template index 2497b0846..e558886a0 100644 --- a/exercises/practice/two-fer/.meta/generator.template +++ b/exercises/practice/two-fer/.meta/generator.template @@ -1,10 +1,8 @@ (ns two-fer-test (:require [clojure.test :refer [deftest testing is]] two-fer)) - {{#test_cases}} (deftest two-fer_test_{{idx}} (testing "{{description}}" (is (= "{{expected}}" (two-fer/two-fer{{#input.name}} "{{input.name}}"{{/input.name}}))))) - {{/test_cases}} diff --git a/generators/deps.edn b/generators/deps.edn index 0e9843006..7a1fde6fb 100644 --- a/generators/deps.edn +++ b/generators/deps.edn @@ -1,5 +1,5 @@ {:paths ["src"] :deps {org.clojure/data.json {:mvn/version "2.5.1"} - pogonos/pogonos {:mvn/version "0.2.1"} + hbs/hbs {:mvn/version "1.0.3"} io.github.tonsky/toml-clj {:mvn/version "0.1.0"} clj-jgit/clj-jgit {:mvn/version "1.1.0"}}} diff --git a/generators/src/templates.clj b/generators/src/templates.clj index 59221748e..d2e526031 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -1,6 +1,5 @@ (ns templates - (:require [pogonos.core :as pg] - [pogonos.output :as output] + (:require [hbs.core :as hbs] [log] [paths])) @@ -13,7 +12,7 @@ (set))) (defn generate-tests-file [slug test-cases] - (pg/render-file - (paths/generator-template-file slug) - {:test_cases test-cases} - {:output (output/to-file (paths/tests-file slug))})) + (let [template (slurp (paths/generator-template-file slug)) + data {:test_cases test-cases}] + (->> (hbs/render template data) + (spit (paths/tests-file slug))))) From 9d4f52356fae63dad022cde6884eaea6a8c3c207 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sat, 11 Jan 2025 09:12:16 +0100 Subject: [PATCH 15/26] Refactoring and fix idx per property --- generators/src/canonical_data.clj | 19 +++++-------------- generators/src/generator.clj | 2 +- generators/src/templates.clj | 17 +++++++++++++++-- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/generators/src/canonical_data.clj b/generators/src/canonical_data.clj index f5d74f822..1b9a6bbc0 100644 --- a/generators/src/canonical_data.clj +++ b/generators/src/canonical_data.clj @@ -4,8 +4,7 @@ [toml-clj.core :as toml] [clj-jgit.porcelain :refer [git-clone git-pull load-repo]] [log] - [paths] - [clojure.string :as str])) + [paths])) (def git-url "https://github.com/exercism/problem-specifications.git") @@ -42,27 +41,19 @@ (let [excluded (excluded-uuids slug)] (fn [node] (contains? excluded (:uuid node))))) -(defn- node->test-case [idx node] - (-> node - (assoc :idx (inc idx) - :description (str/join " - " (:path node)) - :error (get-in node [:expected :error])) - (dissoc :reimplements :comments :scenarios))) - -(defn- test-case-nodes - ([node] (test-case-nodes node [])) +(defn- cases + ([node] (cases node [])) ([node path] (let [description (:description node) children (:cases node) updated-path (if description (conj path description) path)] (if children - (mapcat #(test-case-nodes % updated-path) children) + (mapcat #(cases % updated-path) children) [(assoc node :path updated-path)])))) (defn test-cases [slug] (->> slug (canonical-data) - (test-case-nodes) + (cases) (remove (excluded? slug)) - (map-indexed node->test-case) (into []))) diff --git a/generators/src/generator.clj b/generators/src/generator.clj index 7723cc2ad..f41e7a3d4 100644 --- a/generators/src/generator.clj +++ b/generators/src/generator.clj @@ -16,4 +16,4 @@ (canonical-data/sync-repo) (doseq [slug (slugs-to-generate (str exercise))] (log/normal (str "Generating tests for exercise '" slug "'")) - (templates/generate-tests-file slug (canonical-data/test-cases slug)))) + (templates/generate-test-files slug (canonical-data/test-cases slug)))) diff --git a/generators/src/templates.clj b/generators/src/templates.clj index d2e526031..1ad060bb0 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -1,5 +1,6 @@ (ns templates (:require [hbs.core :as hbs] + [clojure.string :as str] [log] [paths])) @@ -11,8 +12,20 @@ (map #(-> % (.getParentFile) (.getParentFile) (.getName))) (set))) -(defn generate-tests-file [slug test-cases] +(defn- test-case->data [idx node] + (-> node + (assoc :idx (inc idx) + :description (str/join " - " (:path node)) + :error (get-in node [:expected :error])) + (dissoc :reimplements :comments :scenarios))) + +(defn- test-cases->data [test-cases] + (let [test-cases-by-property (update-vals (group-by :property test-cases) #(map-indexed test-case->data %))] + {:test_cases (reduce concat (vals test-cases-by-property)) + :test_cases_by_property test-cases-by-property})) + +(defn generate-test-files [slug test-cases] (let [template (slurp (paths/generator-template-file slug)) - data {:test_cases test-cases}] + data (test-cases->data test-cases)] (->> (hbs/render template data) (spit (paths/tests-file slug))))) From ca5d15bb5d6ffd24f328532bf923552635a39eea Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sat, 11 Jan 2025 12:12:37 +0100 Subject: [PATCH 16/26] Allow customization --- .../difference-of-squares/.meta/example.clj | 5 +- .../difference-of-squares/.meta/generator.clj | 7 +++ .../.meta/generator.template | 18 +++++++ .../test/difference_of_squares_test.clj | 50 +++++++++++-------- .../practice/isogram/.meta/generator.template | 4 +- .../roman-numerals/.meta/generator.template | 4 +- .../practice/two-fer/.meta/generator.template | 4 +- generators/src/paths.clj | 1 + generators/src/templates.clj | 21 ++++++-- 9 files changed, 81 insertions(+), 33 deletions(-) create mode 100644 exercises/practice/difference-of-squares/.meta/generator.clj create mode 100644 exercises/practice/difference-of-squares/.meta/generator.template diff --git a/exercises/practice/difference-of-squares/.meta/example.clj b/exercises/practice/difference-of-squares/.meta/example.clj index c67705e8c..3ba7cf98b 100644 --- a/exercises/practice/difference-of-squares/.meta/example.clj +++ b/exercises/practice/difference-of-squares/.meta/example.clj @@ -1,12 +1,13 @@ (ns difference-of-squares) +(defn- square [n] (* n n)) (defn- sum [xs] (reduce + xs)) (defn sum-of-squares [n] - (sum (map #(int (Math/pow % 2)) (range 0 (inc n))))) + (sum (map square (range 1 (inc n))))) (defn square-of-sum [n] - (int (Math/pow (sum (range 0 (inc n))) 2))) + (square (sum (range 1 (inc n))))) (defn difference [x] (- (square-of-sum x) (sum-of-squares x))) diff --git a/exercises/practice/difference-of-squares/.meta/generator.clj b/exercises/practice/difference-of-squares/.meta/generator.clj new file mode 100644 index 000000000..7facb501a --- /dev/null +++ b/exercises/practice/difference-of-squares/.meta/generator.clj @@ -0,0 +1,7 @@ +(ns difference-of-squares-generator) + +(defn- update-path [path] + (take-last 1 path)) + +(defn transform [test-cases] + (map #(update % :path update-path) test-cases)) diff --git a/exercises/practice/difference-of-squares/.meta/generator.template b/exercises/practice/difference-of-squares/.meta/generator.template new file mode 100644 index 000000000..1cce7520f --- /dev/null +++ b/exercises/practice/difference-of-squares/.meta/generator.template @@ -0,0 +1,18 @@ +(ns difference-of-squares-test + (:require [clojure.test :refer [deftest testing is]] + difference-of-squares)) +{{#test_cases.squareOfSum}} +(deftest square-of-sum_test_{{idx}} + (testing "{{description}}" + (is (= {{expected}} (difference-of-squares/square-of-sum {{input.number}}))))) +{{/test_cases.squareOfSum~}} +{{#test_cases.sumOfSquares}} +(deftest sum-of-squares_test_{{idx}} + (testing "{{description}}" + (is (= {{expected}} (difference-of-squares/sum-of-squares {{input.number}}))))) +{{/test_cases.sumOfSquares~}} +{{#test_cases.differenceOfSquares}} +(deftest difference-of-squares_test_{{idx}} + (testing "{{description}}" + (is (= {{expected}} (difference-of-squares/difference {{input.number}}))))) +{{/test_cases.differenceOfSquares}} diff --git a/exercises/practice/difference-of-squares/test/difference_of_squares_test.clj b/exercises/practice/difference-of-squares/test/difference_of_squares_test.clj index 2355c8f60..10e30eb1d 100644 --- a/exercises/practice/difference-of-squares/test/difference_of_squares_test.clj +++ b/exercises/practice/difference-of-squares/test/difference_of_squares_test.clj @@ -1,30 +1,40 @@ (ns difference-of-squares-test - (:require [clojure.test :refer [deftest is]] - [difference-of-squares :as dos])) + (:require [clojure.test :refer [deftest testing is]] + difference-of-squares)) -(deftest square-of-sum-to-5 - (is (= 225 (dos/square-of-sum 5)))) +(deftest square-of-sum_test_1 + (testing "square of sum 1" + (is (= 1 (difference-of-squares/square-of-sum 1))))) -(deftest sum-of-squares-to-5 - (is (= 55 (dos/sum-of-squares 5)))) +(deftest square-of-sum_test_2 + (testing "square of sum 5" + (is (= 225 (difference-of-squares/square-of-sum 5))))) -(deftest difference-of-squares-to-5 - (is (= 170 (dos/difference 5)))) +(deftest square-of-sum_test_3 + (testing "square of sum 100" + (is (= 25502500 (difference-of-squares/square-of-sum 100))))) -(deftest square-of-sum-to-10 - (is (= 3025 (dos/square-of-sum 10)))) +(deftest sum-of-squares_test_1 + (testing "sum of squares 1" + (is (= 1 (difference-of-squares/sum-of-squares 1))))) -(deftest sum-of-squares-to-10 - (is (= 385 (dos/sum-of-squares 10)))) +(deftest sum-of-squares_test_2 + (testing "sum of squares 5" + (is (= 55 (difference-of-squares/sum-of-squares 5))))) -(deftest difference-of-squares-to-10 - (is (= 2640 (dos/difference 10)))) +(deftest sum-of-squares_test_3 + (testing "sum of squares 100" + (is (= 338350 (difference-of-squares/sum-of-squares 100))))) -(deftest square-of-sum-to-100 - (is (= 25502500 (dos/square-of-sum 100)))) +(deftest difference-of-squares_test_1 + (testing "difference of squares 1" + (is (= 0 (difference-of-squares/difference 1))))) -(deftest sum-of-squares-to-100 - (is (= 338350 (dos/sum-of-squares 100)))) +(deftest difference-of-squares_test_2 + (testing "difference of squares 5" + (is (= 170 (difference-of-squares/difference 5))))) + +(deftest difference-of-squares_test_3 + (testing "difference of squares 100" + (is (= 25164150 (difference-of-squares/difference 100))))) -(deftest difference-of-squares-to-100 - (is (= 25164150 (dos/difference 100)))) diff --git a/exercises/practice/isogram/.meta/generator.template b/exercises/practice/isogram/.meta/generator.template index cec89491d..59cd39aba 100644 --- a/exercises/practice/isogram/.meta/generator.template +++ b/exercises/practice/isogram/.meta/generator.template @@ -2,7 +2,7 @@ (:require [clojure.test :refer [deftest testing is]] isogram)) -{{#test_cases~}} +{{#test_cases.isIsogram~}} (deftest isogram_test_{{idx}} (testing "{{description}}" {{#expected~}} @@ -10,4 +10,4 @@ {{else~}} (is (not (isogram/isogram? "{{input.phrase}}"))))) {{/expected}} -{{/test_cases}} +{{/test_cases.isIsogram}} diff --git a/exercises/practice/roman-numerals/.meta/generator.template b/exercises/practice/roman-numerals/.meta/generator.template index 94c3a71db..1a37072ed 100644 --- a/exercises/practice/roman-numerals/.meta/generator.template +++ b/exercises/practice/roman-numerals/.meta/generator.template @@ -1,8 +1,8 @@ (ns roman-numerals-test (:require [clojure.test :refer [deftest testing is]] roman-numerals)) -{{#test_cases}} +{{#test_cases.roman}} (deftest roman-numerals_test_{{idx}} (testing "{{description}}" (is (= "{{expected}}" (roman-numerals/numerals {{input.number}}))))) -{{/test_cases}} +{{/test_cases.roman}} diff --git a/exercises/practice/two-fer/.meta/generator.template b/exercises/practice/two-fer/.meta/generator.template index e558886a0..7b1dbdaf0 100644 --- a/exercises/practice/two-fer/.meta/generator.template +++ b/exercises/practice/two-fer/.meta/generator.template @@ -1,8 +1,8 @@ (ns two-fer-test (:require [clojure.test :refer [deftest testing is]] two-fer)) -{{#test_cases}} +{{#test_cases.twoFer}} (deftest two-fer_test_{{idx}} (testing "{{description}}" (is (= "{{expected}}" (two-fer/two-fer{{#input.name}} "{{input.name}}"{{/input.name}}))))) -{{/test_cases}} +{{/test_cases.twoFer}} diff --git a/generators/src/paths.clj b/generators/src/paths.clj index f11e9b1dd..4f7480d94 100644 --- a/generators/src/paths.clj +++ b/generators/src/paths.clj @@ -10,5 +10,6 @@ (defn canonical-data-file [slug] (io/file prob-specs-dir "exercises" slug "canonical-data.json")) (defn tests-toml-file [slug] (io/file (exercise-dir slug) ".meta" "tests.toml")) (defn generator-template-file [slug] (io/file (exercise-dir slug) ".meta" "generator.template")) +(defn generator-clojure-file [slug] (io/file (exercise-dir slug) ".meta" "generator.clj")) (defn tests-file-name [slug] (str (str/replace slug "-" "_") "_test.clj")) (defn tests-file [slug] (io/file (exercise-dir slug) "test" (tests-file-name slug))) diff --git a/generators/src/templates.clj b/generators/src/templates.clj index 1ad060bb0..d6eb5624a 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -19,13 +19,24 @@ :error (get-in node [:expected :error])) (dissoc :reimplements :comments :scenarios))) -(defn- test-cases->data [test-cases] - (let [test-cases-by-property (update-vals (group-by :property test-cases) #(map-indexed test-case->data %))] - {:test_cases (reduce concat (vals test-cases-by-property)) - :test_cases_by_property test-cases-by-property})) +(defn- transform [slug test-cases] + (let [transform-file (paths/generator-clojure-file slug)] + (if (.exists transform-file) + (let [generator-ns (symbol (str slug "-generator"))] + (load-file (str transform-file)) + (if-let [transform-fn (ns-resolve generator-ns (symbol "transform"))] + (transform-fn test-cases) + test-cases)) + test-cases))) + +(defn- test-cases->data [slug test-cases] + (let [transformed (transform slug test-cases) + grouped (group-by :property transformed) + data (update-vals grouped #(map-indexed test-case->data %))] + {:test_cases data})) (defn generate-test-files [slug test-cases] (let [template (slurp (paths/generator-template-file slug)) - data (test-cases->data test-cases)] + data (test-cases->data slug test-cases)] (->> (hbs/render template data) (spit (paths/tests-file slug))))) From caedd84ee62c1b3c2a33d01af6ffd850685fec54 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sat, 11 Jan 2025 12:26:03 +0100 Subject: [PATCH 17/26] Revert exercises --- .../difference-of-squares/.meta/example.clj | 5 +- .../difference-of-squares/.meta/generator.clj | 7 - .../.meta/generator.template | 18 --- .../test/difference_of_squares_test.clj | 50 +++---- exercises/practice/isogram/.meta/example.clj | 3 +- .../practice/isogram/.meta/generator.template | 13 -- .../practice/isogram/test/isogram_test.clj | 72 ++-------- .../roman-numerals/.meta/generator.template | 8 -- .../test/roman_numerals_test.clj | 131 +++++------------- .../practice/two-fer/.meta/generator.template | 8 -- .../practice/two-fer/test/two_fer_test.clj | 20 ++- 11 files changed, 83 insertions(+), 252 deletions(-) delete mode 100644 exercises/practice/difference-of-squares/.meta/generator.clj delete mode 100644 exercises/practice/difference-of-squares/.meta/generator.template delete mode 100644 exercises/practice/isogram/.meta/generator.template delete mode 100644 exercises/practice/roman-numerals/.meta/generator.template delete mode 100644 exercises/practice/two-fer/.meta/generator.template diff --git a/exercises/practice/difference-of-squares/.meta/example.clj b/exercises/practice/difference-of-squares/.meta/example.clj index 3ba7cf98b..c67705e8c 100644 --- a/exercises/practice/difference-of-squares/.meta/example.clj +++ b/exercises/practice/difference-of-squares/.meta/example.clj @@ -1,13 +1,12 @@ (ns difference-of-squares) -(defn- square [n] (* n n)) (defn- sum [xs] (reduce + xs)) (defn sum-of-squares [n] - (sum (map square (range 1 (inc n))))) + (sum (map #(int (Math/pow % 2)) (range 0 (inc n))))) (defn square-of-sum [n] - (square (sum (range 1 (inc n))))) + (int (Math/pow (sum (range 0 (inc n))) 2))) (defn difference [x] (- (square-of-sum x) (sum-of-squares x))) diff --git a/exercises/practice/difference-of-squares/.meta/generator.clj b/exercises/practice/difference-of-squares/.meta/generator.clj deleted file mode 100644 index 7facb501a..000000000 --- a/exercises/practice/difference-of-squares/.meta/generator.clj +++ /dev/null @@ -1,7 +0,0 @@ -(ns difference-of-squares-generator) - -(defn- update-path [path] - (take-last 1 path)) - -(defn transform [test-cases] - (map #(update % :path update-path) test-cases)) diff --git a/exercises/practice/difference-of-squares/.meta/generator.template b/exercises/practice/difference-of-squares/.meta/generator.template deleted file mode 100644 index 1cce7520f..000000000 --- a/exercises/practice/difference-of-squares/.meta/generator.template +++ /dev/null @@ -1,18 +0,0 @@ -(ns difference-of-squares-test - (:require [clojure.test :refer [deftest testing is]] - difference-of-squares)) -{{#test_cases.squareOfSum}} -(deftest square-of-sum_test_{{idx}} - (testing "{{description}}" - (is (= {{expected}} (difference-of-squares/square-of-sum {{input.number}}))))) -{{/test_cases.squareOfSum~}} -{{#test_cases.sumOfSquares}} -(deftest sum-of-squares_test_{{idx}} - (testing "{{description}}" - (is (= {{expected}} (difference-of-squares/sum-of-squares {{input.number}}))))) -{{/test_cases.sumOfSquares~}} -{{#test_cases.differenceOfSquares}} -(deftest difference-of-squares_test_{{idx}} - (testing "{{description}}" - (is (= {{expected}} (difference-of-squares/difference {{input.number}}))))) -{{/test_cases.differenceOfSquares}} diff --git a/exercises/practice/difference-of-squares/test/difference_of_squares_test.clj b/exercises/practice/difference-of-squares/test/difference_of_squares_test.clj index 10e30eb1d..2355c8f60 100644 --- a/exercises/practice/difference-of-squares/test/difference_of_squares_test.clj +++ b/exercises/practice/difference-of-squares/test/difference_of_squares_test.clj @@ -1,40 +1,30 @@ (ns difference-of-squares-test - (:require [clojure.test :refer [deftest testing is]] - difference-of-squares)) + (:require [clojure.test :refer [deftest is]] + [difference-of-squares :as dos])) -(deftest square-of-sum_test_1 - (testing "square of sum 1" - (is (= 1 (difference-of-squares/square-of-sum 1))))) +(deftest square-of-sum-to-5 + (is (= 225 (dos/square-of-sum 5)))) -(deftest square-of-sum_test_2 - (testing "square of sum 5" - (is (= 225 (difference-of-squares/square-of-sum 5))))) +(deftest sum-of-squares-to-5 + (is (= 55 (dos/sum-of-squares 5)))) -(deftest square-of-sum_test_3 - (testing "square of sum 100" - (is (= 25502500 (difference-of-squares/square-of-sum 100))))) +(deftest difference-of-squares-to-5 + (is (= 170 (dos/difference 5)))) -(deftest sum-of-squares_test_1 - (testing "sum of squares 1" - (is (= 1 (difference-of-squares/sum-of-squares 1))))) +(deftest square-of-sum-to-10 + (is (= 3025 (dos/square-of-sum 10)))) -(deftest sum-of-squares_test_2 - (testing "sum of squares 5" - (is (= 55 (difference-of-squares/sum-of-squares 5))))) +(deftest sum-of-squares-to-10 + (is (= 385 (dos/sum-of-squares 10)))) -(deftest sum-of-squares_test_3 - (testing "sum of squares 100" - (is (= 338350 (difference-of-squares/sum-of-squares 100))))) +(deftest difference-of-squares-to-10 + (is (= 2640 (dos/difference 10)))) -(deftest difference-of-squares_test_1 - (testing "difference of squares 1" - (is (= 0 (difference-of-squares/difference 1))))) +(deftest square-of-sum-to-100 + (is (= 25502500 (dos/square-of-sum 100)))) -(deftest difference-of-squares_test_2 - (testing "difference of squares 5" - (is (= 170 (difference-of-squares/difference 5))))) - -(deftest difference-of-squares_test_3 - (testing "difference of squares 100" - (is (= 25164150 (difference-of-squares/difference 100))))) +(deftest sum-of-squares-to-100 + (is (= 338350 (dos/sum-of-squares 100)))) +(deftest difference-of-squares-to-100 + (is (= 25164150 (dos/difference 100)))) diff --git a/exercises/practice/isogram/.meta/example.clj b/exercises/practice/isogram/.meta/example.clj index 9afdda6f1..ce0b8c146 100644 --- a/exercises/practice/isogram/.meta/example.clj +++ b/exercises/practice/isogram/.meta/example.clj @@ -2,5 +2,4 @@ (:require [clojure.string :as str])) (defn isogram? [word] - (let [letters (filter #(Character/isLetter %) (str/lower-case word))] - (or (empty? letters) (apply distinct? letters)))) + (apply distinct? (filter #(Character/isLetter %) (str/lower-case word)))) diff --git a/exercises/practice/isogram/.meta/generator.template b/exercises/practice/isogram/.meta/generator.template deleted file mode 100644 index 59cd39aba..000000000 --- a/exercises/practice/isogram/.meta/generator.template +++ /dev/null @@ -1,13 +0,0 @@ -(ns isogram-test - (:require [clojure.test :refer [deftest testing is]] - isogram)) - -{{#test_cases.isIsogram~}} -(deftest isogram_test_{{idx}} - (testing "{{description}}" - {{#expected~}} - (is (isogram/isogram? "{{input.phrase}}")))) - {{else~}} - (is (not (isogram/isogram? "{{input.phrase}}"))))) - {{/expected}} -{{/test_cases.isIsogram}} diff --git a/exercises/practice/isogram/test/isogram_test.clj b/exercises/practice/isogram/test/isogram_test.clj index 7b4cbcd7c..5c2cf69a8 100644 --- a/exercises/practice/isogram/test/isogram_test.clj +++ b/exercises/practice/isogram/test/isogram_test.clj @@ -1,61 +1,17 @@ (ns isogram-test - (:require [clojure.test :refer [deftest testing is]] - isogram)) + (:require [clojure.test :refer [deftest is]] + isogram)) -(deftest isogram_test_1 - (testing "empty string" - (is (isogram/isogram? "")))) - -(deftest isogram_test_2 - (testing "isogram with only lower case characters" - (is (isogram/isogram? "isogram")))) - -(deftest isogram_test_3 - (testing "word with one duplicated character" - (is (not (isogram/isogram? "eleven"))))) - -(deftest isogram_test_4 - (testing "word with one duplicated character from the end of the alphabet" - (is (not (isogram/isogram? "zzyzx"))))) - -(deftest isogram_test_5 - (testing "longest reported english isogram" - (is (isogram/isogram? "subdermatoglyphic")))) - -(deftest isogram_test_6 - (testing "word with duplicated character in mixed case" - (is (not (isogram/isogram? "Alphabet"))))) - -(deftest isogram_test_7 - (testing "word with duplicated character in mixed case, lowercase first" - (is (not (isogram/isogram? "alphAbet"))))) - -(deftest isogram_test_8 - (testing "hypothetical isogrammic word with hyphen" - (is (isogram/isogram? "thumbscrew-japingly")))) - -(deftest isogram_test_9 - (testing "hypothetical word with duplicated character following hyphen" - (is (not (isogram/isogram? "thumbscrew-jappingly"))))) - -(deftest isogram_test_10 - (testing "isogram with duplicated hyphen" - (is (isogram/isogram? "six-year-old")))) - -(deftest isogram_test_11 - (testing "made-up name that is an isogram" - (is (isogram/isogram? "Emily Jung Schwartzkopf")))) - -(deftest isogram_test_12 - (testing "duplicated character in the middle" - (is (not (isogram/isogram? "accentor"))))) - -(deftest isogram_test_13 - (testing "same first and last characters" - (is (not (isogram/isogram? "angola"))))) - -(deftest isogram_test_14 - (testing "word with duplicated character and with two hyphens" - (is (not (isogram/isogram? "up-to-date"))))) - +(deftest test-isograms + (is (isogram/isogram? "duplicates")) + (is (isogram/isogram? "subdermatoglyphic")) + (is (isogram/isogram? "thumbscrew-japingly")) + (is (isogram/isogram? "Hjelmqvist-Gryb-Zock-Pfund-Wax")) + (is (isogram/isogram? "Heizölrückstoßabdämpfung")) + (is (isogram/isogram? "Emily Jung Schwartzkopf"))) +(deftest test-non-isograms + (is (not (isogram/isogram? "eleven"))) + (is (not (isogram/isogram? "Alphabet"))) + (is (not (isogram/isogram? "the quick brown fox"))) + (is (not (isogram/isogram? "éléphant")))) diff --git a/exercises/practice/roman-numerals/.meta/generator.template b/exercises/practice/roman-numerals/.meta/generator.template deleted file mode 100644 index 1a37072ed..000000000 --- a/exercises/practice/roman-numerals/.meta/generator.template +++ /dev/null @@ -1,8 +0,0 @@ -(ns roman-numerals-test - (:require [clojure.test :refer [deftest testing is]] - roman-numerals)) -{{#test_cases.roman}} -(deftest roman-numerals_test_{{idx}} - (testing "{{description}}" - (is (= "{{expected}}" (roman-numerals/numerals {{input.number}}))))) -{{/test_cases.roman}} diff --git a/exercises/practice/roman-numerals/test/roman_numerals_test.clj b/exercises/practice/roman-numerals/test/roman_numerals_test.clj index 47c345043..967f02a3f 100644 --- a/exercises/practice/roman-numerals/test/roman_numerals_test.clj +++ b/exercises/practice/roman-numerals/test/roman_numerals_test.clj @@ -1,112 +1,57 @@ (ns roman-numerals-test - (:require [clojure.test :refer [deftest testing is]] - roman-numerals)) + (:require [clojure.test :refer [deftest is]] + roman-numerals)) -(deftest roman-numerals_test_1 - (testing "1 is I" - (is (= "I" (roman-numerals/numerals 1))))) +(deftest one + (is (= "I" (roman-numerals/numerals 1)))) -(deftest roman-numerals_test_2 - (testing "2 is II" - (is (= "II" (roman-numerals/numerals 2))))) +(deftest two + (is (= "II" (roman-numerals/numerals 2)))) -(deftest roman-numerals_test_3 - (testing "3 is III" - (is (= "III" (roman-numerals/numerals 3))))) +(deftest three + (is (= "III" (roman-numerals/numerals 3)))) -(deftest roman-numerals_test_4 - (testing "4 is IV" - (is (= "IV" (roman-numerals/numerals 4))))) +(deftest four + (is (= "IV" (roman-numerals/numerals 4)))) -(deftest roman-numerals_test_5 - (testing "5 is V" - (is (= "V" (roman-numerals/numerals 5))))) +(deftest five + (is (= "V" (roman-numerals/numerals 5)))) -(deftest roman-numerals_test_6 - (testing "6 is VI" - (is (= "VI" (roman-numerals/numerals 6))))) +(deftest six + (is (= "VI" (roman-numerals/numerals 6)))) -(deftest roman-numerals_test_7 - (testing "9 is IX" - (is (= "IX" (roman-numerals/numerals 9))))) +(deftest nine + (is (= "IX" (roman-numerals/numerals 9)))) -(deftest roman-numerals_test_8 - (testing "16 is XVI" - (is (= "XVI" (roman-numerals/numerals 16))))) +(deftest twenty-seven + (is (= "XXVII" (roman-numerals/numerals 27)))) -(deftest roman-numerals_test_9 - (testing "27 is XXVII" - (is (= "XXVII" (roman-numerals/numerals 27))))) +(deftest forty-eight + (is (= "XLVIII" (roman-numerals/numerals 48)))) -(deftest roman-numerals_test_10 - (testing "48 is XLVIII" - (is (= "XLVIII" (roman-numerals/numerals 48))))) +(deftest fifty-nine + (is (= "LIX" (roman-numerals/numerals 59)))) -(deftest roman-numerals_test_11 - (testing "49 is XLIX" - (is (= "XLIX" (roman-numerals/numerals 49))))) +(deftest ninety-three + (is (= "XCIII" (roman-numerals/numerals 93)))) -(deftest roman-numerals_test_12 - (testing "59 is LIX" - (is (= "LIX" (roman-numerals/numerals 59))))) +(deftest one-hundred-forty-one + (is (= "CXLI" (roman-numerals/numerals 141)))) -(deftest roman-numerals_test_13 - (testing "66 is LXVI" - (is (= "LXVI" (roman-numerals/numerals 66))))) +(deftest one-hundred-sixty-three + (is (= "CLXIII" (roman-numerals/numerals 163)))) -(deftest roman-numerals_test_14 - (testing "93 is XCIII" - (is (= "XCIII" (roman-numerals/numerals 93))))) +(deftest four-hundred-two + (is (= "CDII" (roman-numerals/numerals 402)))) -(deftest roman-numerals_test_15 - (testing "141 is CXLI" - (is (= "CXLI" (roman-numerals/numerals 141))))) +(deftest five-hundred-seventy-five + (is (= "DLXXV" (roman-numerals/numerals 575)))) -(deftest roman-numerals_test_16 - (testing "163 is CLXIII" - (is (= "CLXIII" (roman-numerals/numerals 163))))) +(deftest nine-hundred-eleven + (is (= "CMXI" (roman-numerals/numerals 911)))) -(deftest roman-numerals_test_17 - (testing "166 is CLXVI" - (is (= "CLXVI" (roman-numerals/numerals 166))))) - -(deftest roman-numerals_test_18 - (testing "402 is CDII" - (is (= "CDII" (roman-numerals/numerals 402))))) - -(deftest roman-numerals_test_19 - (testing "575 is DLXXV" - (is (= "DLXXV" (roman-numerals/numerals 575))))) - -(deftest roman-numerals_test_20 - (testing "666 is DCLXVI" - (is (= "DCLXVI" (roman-numerals/numerals 666))))) - -(deftest roman-numerals_test_21 - (testing "911 is CMXI" - (is (= "CMXI" (roman-numerals/numerals 911))))) - -(deftest roman-numerals_test_22 - (testing "1024 is MXXIV" - (is (= "MXXIV" (roman-numerals/numerals 1024))))) - -(deftest roman-numerals_test_23 - (testing "1666 is MDCLXVI" - (is (= "MDCLXVI" (roman-numerals/numerals 1666))))) - -(deftest roman-numerals_test_24 - (testing "3000 is MMM" - (is (= "MMM" (roman-numerals/numerals 3000))))) - -(deftest roman-numerals_test_25 - (testing "3001 is MMMI" - (is (= "MMMI" (roman-numerals/numerals 3001))))) - -(deftest roman-numerals_test_26 - (testing "3888 is MMMDCCCLXXXVIII" - (is (= "MMMDCCCLXXXVIII" (roman-numerals/numerals 3888))))) - -(deftest roman-numerals_test_27 - (testing "3999 is MMMCMXCIX" - (is (= "MMMCMXCIX" (roman-numerals/numerals 3999))))) +(deftest one-thousand-twenty-four + (is (= "MXXIV" (roman-numerals/numerals 1024)))) +(deftest three-thousand + (is (= "MMM" (roman-numerals/numerals 3000)))) diff --git a/exercises/practice/two-fer/.meta/generator.template b/exercises/practice/two-fer/.meta/generator.template deleted file mode 100644 index 7b1dbdaf0..000000000 --- a/exercises/practice/two-fer/.meta/generator.template +++ /dev/null @@ -1,8 +0,0 @@ -(ns two-fer-test - (:require [clojure.test :refer [deftest testing is]] - two-fer)) -{{#test_cases.twoFer}} -(deftest two-fer_test_{{idx}} - (testing "{{description}}" - (is (= "{{expected}}" (two-fer/two-fer{{#input.name}} "{{input.name}}"{{/input.name}}))))) -{{/test_cases.twoFer}} diff --git a/exercises/practice/two-fer/test/two_fer_test.clj b/exercises/practice/two-fer/test/two_fer_test.clj index 6be75ad22..a1ac32968 100644 --- a/exercises/practice/two-fer/test/two_fer_test.clj +++ b/exercises/practice/two-fer/test/two_fer_test.clj @@ -1,16 +1,12 @@ (ns two-fer-test - (:require [clojure.test :refer [deftest testing is]] - two-fer)) + (:require [clojure.test :refer [deftest is]] + two-fer)) -(deftest two-fer_test_1 - (testing "no name given" - (is (= "One for you, one for me." (two-fer/two-fer))))) +(deftest two-fer-test + (is (= "One for you, one for me." (two-fer/two-fer)))) -(deftest two-fer_test_2 - (testing "a name given" - (is (= "One for Alice, one for me." (two-fer/two-fer "Alice"))))) - -(deftest two-fer_test_3 - (testing "another name given" - (is (= "One for Bob, one for me." (two-fer/two-fer "Bob"))))) +(deftest name-alice-test + (is (= "One for Alice, one for me." (two-fer/two-fer "Alice")))) +(deftest name-bob-test + (is (= "One for Bob, one for me." (two-fer/two-fer "Bob")))) From af791cba7257495ecfa3912ffad931016959654c Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sat, 11 Jan 2025 15:11:41 +0100 Subject: [PATCH 18/26] Change extension --- generators/src/paths.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/src/paths.clj b/generators/src/paths.clj index 4f7480d94..745f2f4da 100644 --- a/generators/src/paths.clj +++ b/generators/src/paths.clj @@ -9,7 +9,7 @@ (defn exercise-dir [slug] (io/file exercises-dir slug)) (defn canonical-data-file [slug] (io/file prob-specs-dir "exercises" slug "canonical-data.json")) (defn tests-toml-file [slug] (io/file (exercise-dir slug) ".meta" "tests.toml")) -(defn generator-template-file [slug] (io/file (exercise-dir slug) ".meta" "generator.template")) +(defn generator-template-file [slug] (io/file (exercise-dir slug) ".meta" "generator.tpl")) (defn generator-clojure-file [slug] (io/file (exercise-dir slug) ".meta" "generator.clj")) (defn tests-file-name [slug] (str (str/replace slug "-" "_") "_test.clj")) (defn tests-file [slug] (io/file (exercise-dir slug) "test" (tests-file-name slug))) From 9c27978b0b7c23c64b7a765dcf2cf9241926d6f4 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sat, 11 Jan 2025 18:21:45 +0100 Subject: [PATCH 19/26] Use root for prob-specs repo --- generators/src/paths.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/src/paths.clj b/generators/src/paths.clj index 745f2f4da..d03e79920 100644 --- a/generators/src/paths.clj +++ b/generators/src/paths.clj @@ -4,7 +4,7 @@ (def generators-dir (.getCanonicalPath (io/file "."))) (def root-dir (.getCanonicalPath (io/file generators-dir ".."))) -(def prob-specs-dir (io/file generators-dir ".problem-specifications")) +(def prob-specs-dir (io/file root-dir ".problem-specifications")) (def exercises-dir (io/file root-dir "exercises" "practice")) (defn exercise-dir [slug] (io/file exercises-dir slug)) (defn canonical-data-file [slug] (io/file prob-specs-dir "exercises" slug "canonical-data.json")) From 79a58455b6cdef48a673a07bbd40d4473bd54f68 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sun, 12 Jan 2025 08:15:17 +0100 Subject: [PATCH 20/26] Update templates.clj Co-authored-by: Anastasios Chatzialexiou <16361161+tasxatzial@users.noreply.github.com> --- generators/src/templates.clj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generators/src/templates.clj b/generators/src/templates.clj index d6eb5624a..5a3f5362e 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -8,7 +8,7 @@ (->> paths/exercises-dir (file-seq) (filter #(.isFile %)) - (filter #(= "generator.template" (.getName %))) + (filter #(= "generator.tpl" (.getName %))) (map #(-> % (.getParentFile) (.getParentFile) (.getName))) (set))) From 17747ea501b39d145d2b4d7e1ed81107d2d5d233 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sat, 11 Jan 2025 21:15:40 +0100 Subject: [PATCH 21/26] Fix extension --- .../practice/sum-of-multiples/.meta/generator.tpl | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 exercises/practice/sum-of-multiples/.meta/generator.tpl diff --git a/exercises/practice/sum-of-multiples/.meta/generator.tpl b/exercises/practice/sum-of-multiples/.meta/generator.tpl new file mode 100644 index 000000000..42ac0222f --- /dev/null +++ b/exercises/practice/sum-of-multiples/.meta/generator.tpl @@ -0,0 +1,13 @@ +(ns sum-of-multiples-test + (:require [clojure.test :refer [deftest testing is]] + sum-of-multiples)) + +{{#test_cases.sum~}} +(deftest sum-of-multiples?_test_{{idx}} + (testing "{{description}}" + {{#ifequals expected compare=0~}} + (is (zero? (sum-of-multiples/sum-of-multiples {{input.factors}} {{input.limit}}))))) + {{else~}} + (is (= {{expected}} (sum-of-multiples/sum-of-multiples {{input.factors}} {{input.limit}}))))) + {{/ifequals}} +{{/test_cases.sum}} \ No newline at end of file From 7fa9f91a7d30d12be0b9945ea7404f7903c52d23 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sat, 11 Jan 2025 21:36:29 +0100 Subject: [PATCH 22/26] Add helpers --- generators/src/templates.clj | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/generators/src/templates.clj b/generators/src/templates.clj index 5a3f5362e..25fee4645 100644 --- a/generators/src/templates.clj +++ b/generators/src/templates.clj @@ -1,9 +1,21 @@ (ns templates - (:require [hbs.core :as hbs] + (:require [hbs.core :refer [*hbs* render]] + [hbs.helper :refer [defhelper register-helper! safe-str]] + [hbs.ext :refer :all :exclude [hash]] [clojure.string :as str] [log] [paths])) +(defhelper list-helper [ctx options] + (safe-str (str "'" (seq ctx)))) + +(register-helper! *hbs* "list" list-helper) +(register-helper! *hbs* "ifequals" ifequals) +(register-helper! *hbs* "ifgreater" ifgreater) +(register-helper! *hbs* "ifless" ifless) +(register-helper! *hbs* "ifcontains" ifcontains) +(register-helper! *hbs* "ifempty" ifempty) + (def exercises-with-template (->> paths/exercises-dir (file-seq) @@ -38,5 +50,5 @@ (defn generate-test-files [slug test-cases] (let [template (slurp (paths/generator-template-file slug)) data (test-cases->data slug test-cases)] - (->> (hbs/render template data) + (->> (render template data) (spit (paths/tests-file slug))))) From f6bcb12330e4e6a97456e254011b1b6342bd7d2d Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sun, 12 Jan 2025 14:58:38 +0100 Subject: [PATCH 23/26] Update add-practice-exercise script to reference test generator (#738) --- bin/add-practice-exercise | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/bin/add-practice-exercise b/bin/add-practice-exercise index f27001d34..a2260a0bf 100755 --- a/bin/add-practice-exercise +++ b/bin/add-practice-exercise @@ -69,7 +69,10 @@ fi exercise_dir="exercises/practice/${slug}" config_json_file="${exercise_dir}/.meta/config.json" +generator_tpl_file="${exercise_dir}/.meta/generator.tpl" files=$(jq -r --arg dir "${exercise_dir}" '.files | to_entries | map({key: .key, value: (.value | map("'"'"'" + $dir + "/" + . + "'"'"'") | join(" and "))}) | from_entries' "${config_json_file}") +prob_specs_dir=$(./bin/configlet info --verbosity detailed | head -n 1 | sed 's/.*dir: //') +canonical_data_json_file="${prob_specs_dir}/exercises/${slug}/canonical-data.json" sample_exercise_dir="exercises/practice/acronym" for sample_file in deps.edn project.clj; do @@ -96,11 +99,28 @@ for file_type in solution example; do FILE done -cat << NEXT_STEPS -Your next steps are: +if [[ -f "${prob_specs_dir}/exercises/${slug}/canonical-data.json" ]]; then + cp "${exercise_dir}/${test_file}" "${generator_tpl_file}" + + TEST_STEPS=$(cat << EOF +- Create the test generator in '${generator_tpl_file}' + - The test generator uses the canonical data from 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json' + - Any test cases you don't want to implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false" + - Run 'bin/generate-exercise-tests ${slug}' to generate the tests +EOF +) +else + TEST_STEPS=$(cat << EOF - Create the test suite in $(jq -r '.test' <<< "${files}") - The tests should be based on the canonical data at 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json' - Any test cases you don't implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false" +EOF +) +fi + +cat << NEXT_STEPS +Your next steps are: +${TEST_STEPS} - Create the example solution in $(jq -r '.example' <<< "${files}") - Verify the example solution passes the tests by running 'bin/verify-exercises ${slug}' - Create the stub solution in $(jq -r '.solution' <<< "${files}") From 92a3cb225f76486113adc2c5f6c625a33aa60632 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sun, 12 Jan 2025 14:59:26 +0100 Subject: [PATCH 24/26] Update add-practice-exercise --- bin/add-practice-exercise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/add-practice-exercise b/bin/add-practice-exercise index a2260a0bf..0d5a2b522 100755 --- a/bin/add-practice-exercise +++ b/bin/add-practice-exercise @@ -106,7 +106,7 @@ if [[ -f "${prob_specs_dir}/exercises/${slug}/canonical-data.json" ]]; then - Create the test generator in '${generator_tpl_file}' - The test generator uses the canonical data from 'https://github.com/exercism/problem-specifications/blob/main/exercises/${slug}/canonical-data.json' - Any test cases you don't want to implement, mark them in 'exercises/practice/${slug}/.meta/tests.toml' with "include = false" - - Run 'bin/generate-exercise-tests ${slug}' to generate the tests + - Run 'bin/generate-tests ${slug}' to generate the tests EOF ) else From 8bdb91161b6bd412c870c7a361d1f1403ac22b63 Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sun, 12 Jan 2025 15:18:50 +0100 Subject: [PATCH 25/26] Add generator docs (#739) --- docs/GENERATORS.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++ docs/config.json | 7 +++++ 2 files changed, 75 insertions(+) create mode 100644 docs/GENERATORS.md diff --git a/docs/GENERATORS.md b/docs/GENERATORS.md new file mode 100644 index 000000000..c963c4cd2 --- /dev/null +++ b/docs/GENERATORS.md @@ -0,0 +1,68 @@ +# Generators + +The Clojure track uses a [test generator](https://exercism.org/docs/building/tooling/test-generators) to auto-generate practice exercise tests. +It uses the fact that most exercises defined in the [problem-specifications repo](https://github.com/exercism/problem-specifications/) also have a `canonical-data.json` file, which contains standardized test inputs and outputs that can be used to implement the exercise. + +## Steps + +To generate a practice exercise's tests, the test generator: + +1. Reads the exercise's test cases from its [`canonical-data.json` file] +2. Uses `tests.toml` file to omit and excluded test cases +3. Transforms the test cases (optional) +4. Renders the test cases using the exercise's generator template +5. Writes the rendered template to the exercise's test file + +### Step 1: read `canonical-data.json` file + +The test generator parses the test cases from the exercise's `canonical-data.json` using the [clojure/data.json library](https://github.com/clojure/data.json). + +Since some canonical data uses nesting, the parsed test case includes an additional `path` field that contains the `description` properties of any parent elements, as well as the test case's own `description` property. + +Note: keys are parsed as keywords. + +### Step 2: omit excluded tests from `tests.toml` file + +Each exercise has a `tests.toml` file, in which individual tests can be excluded/disabled. +The test generator will remove any test cases that are marked as excluded (`include = false`). + +### Step 3: transform the test cases (optional) + +Some exercises might need some tweaks before rendering the data. +For example, you might want to make the description less verbose. + +To tweak the test cases, define a `.meta/generator.clj` file with a `-generator` namespace . +Then, define a function called `transform` that takes a single argument — the parsed test cases — and returns the transformed test cases. + +Example: + +```clojure +(ns difference-of-squares-generator) + +(defn- update-path [path] + (take-last 1 path)) + +(defn transform [test-cases] + (map #(update % :path update-path) test-cases)) +``` + +This step is entirely optional. + +### Step 4: render the test cases + +The (potentially transformed) test cases are then passed to the `.meta/generator.tpl` file, which defines how the tests should be rendered based on those test cases. + +### Step 5: write the rendered template to the exercise's test file + +Finally, the output of the rendered template is written to the exercise's test file. + +## Templates + +The templates are rendered using the [hbs library](https://github.com/sunng87/hbs), which supports handlebars syntax (using [handlebars.java](https://github.com/jknack/handlebars.java/)). + +## Command-line interface + +There are two ways in which the test generator can be run: + +1. `bin/generate-tests`: generate the tests for all exercises that have a generator template +2. `bin/generate-tests `: generate the tests for the specified exercise, if it has a generator template diff --git a/docs/config.json b/docs/config.json index 3abffac53..f1e29be50 100644 --- a/docs/config.json +++ b/docs/config.json @@ -27,6 +27,13 @@ "path": "docs/RESOURCES.md", "title": "Useful Clojure resources", "blurb": "A collection of useful resources to help you master Clojure" + }, + { + "uuid": "b091add0-0047-42ad-ae0c-adeebf984365", + "slug": "generators", + "path": "docs/GENERATORS.md", + "title": "Learn about test generators", + "blurb": "Learn how the Clojure track uses test generators to generate tests for exercises" } ] } From 432d82de1ca84d4cded019092f912467e7231e4a Mon Sep 17 00:00:00 2001 From: Erik Schierboom Date: Sun, 12 Jan 2025 15:56:19 +0100 Subject: [PATCH 26/26] Delete exercises/practice/sum-of-multiples/.meta/generator.tpl --- .../practice/sum-of-multiples/.meta/generator.tpl | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 exercises/practice/sum-of-multiples/.meta/generator.tpl diff --git a/exercises/practice/sum-of-multiples/.meta/generator.tpl b/exercises/practice/sum-of-multiples/.meta/generator.tpl deleted file mode 100644 index 42ac0222f..000000000 --- a/exercises/practice/sum-of-multiples/.meta/generator.tpl +++ /dev/null @@ -1,13 +0,0 @@ -(ns sum-of-multiples-test - (:require [clojure.test :refer [deftest testing is]] - sum-of-multiples)) - -{{#test_cases.sum~}} -(deftest sum-of-multiples?_test_{{idx}} - (testing "{{description}}" - {{#ifequals expected compare=0~}} - (is (zero? (sum-of-multiples/sum-of-multiples {{input.factors}} {{input.limit}}))))) - {{else~}} - (is (= {{expected}} (sum-of-multiples/sum-of-multiples {{input.factors}} {{input.limit}}))))) - {{/ifequals}} -{{/test_cases.sum}} \ No newline at end of file