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/_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/add-practice-exercise b/bin/add-practice-exercise index f27001d34..0d5a2b522 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-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}") 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/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" } ] } diff --git a/generators/deps.edn b/generators/deps.edn new file mode 100644 index 000000000..7a1fde6fb --- /dev/null +++ b/generators/deps.edn @@ -0,0 +1,5 @@ +{:paths ["src"] + :deps {org.clojure/data.json {:mvn/version "2.5.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/canonical_data.clj b/generators/src/canonical_data.clj new file mode 100644 index 000000000..1b9a6bbc0 --- /dev/null +++ b/generators/src/canonical_data.clj @@ -0,0 +1,59 @@ +(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])) + +(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- cases + ([node] (cases node [])) + ([node path] + (let [description (:description node) + children (:cases node) + updated-path (if description (conj path description) path)] + (if children + (mapcat #(cases % updated-path) children) + [(assoc node :path updated-path)])))) + +(defn test-cases [slug] + (->> slug + (canonical-data) + (cases) + (remove (excluded? slug)) + (into []))) diff --git a/generators/src/generator.clj b/generators/src/generator.clj new file mode 100644 index 000000000..f41e7a3d4 --- /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-test-files 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..d03e79920 --- /dev/null +++ b/generators/src/paths.clj @@ -0,0 +1,15 @@ +(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 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")) +(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.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))) diff --git a/generators/src/templates.clj b/generators/src/templates.clj new file mode 100644 index 000000000..25fee4645 --- /dev/null +++ b/generators/src/templates.clj @@ -0,0 +1,54 @@ +(ns templates + (: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) + (filter #(.isFile %)) + (filter #(= "generator.tpl" (.getName %))) + (map #(-> % (.getParentFile) (.getParentFile) (.getName))) + (set))) + +(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- 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 slug test-cases)] + (->> (render template data) + (spit (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"]])