diff --git a/.gitignore b/.gitignore index a943c431f..014a99f9b 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ pom.xml.asc **/*/*.ipynb_checkpoints concepts/functions-generating-functions/introduction_files/ concepts/functions-generating-functions/*.html +.problem-specifications/ +.cpcache/ 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 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/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 new file mode 100644 index 000000000..d49c8d19d --- /dev/null +++ b/exercises/practice/isogram/.meta/generator.template @@ -0,0 +1,15 @@ +(ns isogram-test + (:require [clojure.test :refer [deftest testing is]] + isogram)) + +{{#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 5c2cf69a8..c727488bc 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/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)))) diff --git a/exercises/practice/two-fer/.meta/generator.template b/exercises/practice/two-fer/.meta/generator.template new file mode 100644 index 000000000..2497b0846 --- /dev/null +++ b/exercises/practice/two-fer/.meta/generator.template @@ -0,0 +1,10 @@ +(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/exercises/practice/two-fer/test/two_fer_test.clj b/exercises/practice/two-fer/test/two_fer_test.clj index a1ac32968..6be75ad22 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")))) diff --git a/generators/deps.edn b/generators/deps.edn new file mode 100644 index 000000000..0e9843006 --- /dev/null +++ b/generators/deps.edn @@ -0,0 +1,5 @@ +{:paths ["src"] + :deps {org.clojure/data.json {:mvn/version "2.5.1"} + 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"}}} diff --git a/generators/src/canonical_data.clj b/generators/src/canonical_data.clj new file mode 100644 index 000000000..f5d74f822 --- /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] + [toml-clj.core :as toml] + [clj-jgit.porcelain :refer [git-clone git-pull load-repo]] + [log] + [paths] + [clojure.string :as str])) + +(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 [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 [])) + ([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) + [(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/generator.clj b/generators/src/generator.clj new file mode 100644 index 000000000..7723cc2ad --- /dev/null +++ b/generators/src/generator.clj @@ -0,0 +1,19 @@ +(ns generator + (:require [clojure.string :as str] + [canonical-data] + [templates] + [log])) + +(defn- slugs-to-generate [slug] + (let [slugs templates/exercises-with-template] + (if (str/blank? slug) + slugs + (if (contains? slugs slug) + [slug] + (log/error (str "No template found for exercise '" slug "'")))))) + +(defn- run [{:keys [exercise]}] + (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)))) diff --git a/generators/src/log.clj b/generators/src/log.clj new file mode 100644 index 000000000..4f31d44d1 --- /dev/null +++ b/generators/src/log.clj @@ -0,0 +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 new file mode 100644 index 000000000..f11e9b1dd --- /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 "."))) +(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 (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 new file mode 100644 index 000000000..59221748e --- /dev/null +++ b/generators/src/templates.clj @@ -0,0 +1,19 @@ +(ns templates + (:require [pogonos.core :as pg] + [pogonos.output :as output] + [log] + [paths])) + +(def exercises-with-template + (->> paths/exercises-dir + (file-seq) + (filter #(.isFile %)) + (filter #(= "generator.template" (.getName %))) + (map #(-> % (.getParentFile) (.getParentFile) (.getName))) + (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))})) diff --git a/project.clj b/project.clj deleted file mode 100644 index 1ba2e647f..000000000 --- a/project.clj +++ /dev/null @@ -1,9 +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"]])