Skip to content

Commit 19a0b96

Browse files
authored
Refactor/document test suite (#436)
* refactor CI script * fix paths * restore .gitignore * document test suite * get root dir from shell * check if args empty * check if root path exists * check if test-runner exists * include test suite output * fix configlet badge
1 parent e26bed4 commit 19a0b96

File tree

4 files changed

+229
-73
lines changed

4 files changed

+229
-73
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Exercism Clojure Track
2+
3+
[![Configlet CI](https://github.com/exercism/clojure/actions/workflows/configlet.yml/badge.svg)](https://github.com/exercism/clojure/actions/workflows/configlet.yml)
4+
[![test](https://github.com/exercism/clojure/workflows/test/badge.svg)](https://github.com/exercism/clojure/actions?query=workflow%3Atest)
5+
6+
**Exercism exercises in [Clojure](https://clojure.org/)**
7+
8+
This is the Clojure track, one of the many tracks on [Exercism][web-exercism].
9+
It holds all the _exercises_ that are currently implemented and available for students to complete.
10+
The track consists of various **concept** exercises which teach the [Clojure syllabus][web-syllabus], and various practice exercises, which are unlocked by progressing in the syllabus and can be used to practice concepts learned.
11+
You can find this in the [`config.json`][file-config].
12+
13+
## Running the test suite
14+
15+
To test all exercises with sample solutions using [babashka](https://babashka.org/):
16+
17+
```bash
18+
./test.clj .
19+
```
20+
{:tested 86, :fails ()}
21+
22+
## Contributing Guide
23+
24+
Please see the [contributing guide](https://exercism.org/docs/building).
25+
26+
[web-exercism]: https://exercism.org
27+
[web-syllabus]: https://exercism.org/tracks/clojure/concepts
28+
[file-config]: https://github.com/exercism/clojure/blob/main/config.json

README.org

Lines changed: 0 additions & 16 deletions
This file was deleted.

test-runner.clj

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
#!/usr/bin/env bb
2+
3+
(require '[clojure.test :as t :refer [is deftest]]
4+
'[babashka.classpath :as cp]
5+
'[cheshire.core :as json]
6+
'[clojure.string :as str]
7+
'[rewrite-clj.zip :as z])
8+
9+
(comment
10+
(def slug "leap")
11+
(def in-dir "/home/porky/exercism/clojure-test-runner/tests/example-success/")
12+
)
13+
14+
;; Add solution source and tests to classpath
15+
(def slug (first *command-line-args*))
16+
(def in-dir (second *command-line-args*))
17+
(def test-ns (symbol (str slug "-test")))
18+
(cp/add-classpath (str in-dir "src:" in-dir "test"))
19+
(require test-ns)
20+
21+
;; Parse test file into zipper using rewrite-clj
22+
(def zloc (z/of-file (str in-dir "/test/" (str/replace slug "-" "_") "_test.clj")))
23+
24+
(defmethod t/report :fail [m])
25+
26+
(defn eval-is [assertion]
27+
(try (eval assertion)
28+
(catch Exception e
29+
(str e))))
30+
31+
(defn test-meta [loc]
32+
(-> loc
33+
(z/find-tag z/next :meta)
34+
first
35+
:children
36+
first
37+
:k))
38+
39+
(defn test-deftest
40+
"Traverses a zipper from a 'deftest node. Recursively
41+
evaluates all assertions and outputs a map of the results."
42+
[loc]
43+
(let [test loc]
44+
(loop [loc test prefix-string ""
45+
test-strings [] results [] assertions []]
46+
(cond
47+
(= (symbol 'deftest) (-> loc z/down z/sexpr))
48+
(recur (-> loc z/down z/right z/right)
49+
prefix-string test-strings results assertions)
50+
(and
51+
(= (symbol 'testing) (-> loc z/down z/sexpr))
52+
(= (symbol 'testing) (-> loc z/down z/right z/right z/down z/sexpr)))
53+
(recur (-> loc z/down z/right z/right)
54+
(-> loc z/down z/right z/sexpr)
55+
test-strings results assertions)
56+
(= (symbol 'testing) (-> loc z/down z/sexpr))
57+
(recur (-> loc z/down z/right z/right)
58+
prefix-string
59+
(conj test-strings
60+
(str/trim (str prefix-string " "
61+
(-> loc z/down z/right z/sexpr))))
62+
(conj results [])
63+
assertions)
64+
(and
65+
(= (symbol 'is) (-> loc z/down z/sexpr))
66+
(= (symbol 'is) (-> loc z/right z/down z/sexpr)))
67+
(recur (-> loc z/right)
68+
prefix-string
69+
test-strings
70+
(conj results [(eval-is (-> loc z/sexpr))])
71+
(conj assertions (z/sexpr loc)))
72+
(= (symbol 'is) (-> loc z/down z/sexpr))
73+
(recur (if (= (symbol 'testing) (-> loc z/up z/right z/sexpr))
74+
(-> loc z/up z/right)
75+
(-> loc z/right))
76+
prefix-string
77+
test-strings
78+
(conj (vec (butlast results))
79+
(conj (vec (last results)) (eval-is (-> loc z/sexpr))))
80+
(conj assertions (z/sexpr loc)))
81+
:else
82+
{:test-name (-> test z/down z/right z/sexpr str)
83+
:results (vec (remove empty? results))
84+
:test-strings test-strings
85+
:assertions assertions
86+
:task_id (when (number? (test-meta test)) (Integer/parseInt (str/replace (str (test-meta test)) ":task" "")))}))))
87+
88+
(comment
89+
(test-deftest (z/of-string "(deftest year-not-divisible-by-4 (is (not (leap/leap-year? 2015))))")
90+
)
91+
(test-deftest (-> zloc z/right))
92+
)
93+
94+
(defn test-file
95+
"Takes a zipper representing a parsed test file.
96+
Finds each 'deftest form, tests it, and outputs
97+
an ordered sequence of result maps."
98+
[loc]
99+
(loop [loc loc
100+
tests []]
101+
(cond
102+
(nil? loc) tests
103+
(= (symbol 'deftest) (-> loc z/down z/sexpr))
104+
(recur (-> loc z/right) (conj tests (test-deftest loc)))
105+
:else
106+
(recur (-> loc z/right) tests))))
107+
108+
(comment
109+
(test-file zloc))
110+
111+
(defn results
112+
"Takes a zipper representing a parsed test file.
113+
Outputs the test results according to the spec."
114+
[loc]
115+
(flatten
116+
(for [test (test-file loc)]
117+
(if (empty? (:test-strings test))
118+
{:name (:test-name test)
119+
:status (if (every? true? (flatten (:results test)))
120+
"pass" "fail")
121+
:test_code (str (first (:assertions test)))}
122+
(for [n (range (count (:test-strings test)))]
123+
{:name (get (:test-strings test) n)
124+
:status (cond
125+
(every? true? (get (:results test) n)) "pass"
126+
(some false? (get (:results test) n)) "fail"
127+
:else "error")
128+
:test_code (str (get (:assertions test) n))
129+
:task_id (:task_id test)})))))
130+
131+
(comment
132+
(first (test-file zloc))
133+
(results zloc)
134+
(eval-is '(is (= true (annalyns-infiltration/can-fast-attack? false))))
135+
(eval-is '(is (= true (annas-infiltration/can-fast-attack? false))))
136+
(eval-is '(is (= false (annalyns-infiltration/can-fast-attack? e))))
137+
(results zloc)
138+
)
139+
140+
;; Produce JSON output
141+
142+
(println (json/generate-string
143+
{:version 3
144+
:status (cond
145+
(every? #(= "pass" (:status %)) (results zloc)) "pass"
146+
(some #(= "fail" (:status %)) (results zloc)) "fail"
147+
:else "error")
148+
:message
149+
(first (remove #(or (= "pass" (:status %))
150+
(= "fail" (:status %)))
151+
(results zloc)))
152+
:tests
153+
(vec (results zloc))}
154+
{:pretty true}))
155+
156+
(System/exit 0)

test.clj

Lines changed: 45 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,75 +3,63 @@
33
(require
44
'[cheshire.core :as json]
55
'[clojure.string :as str]
6-
'[babashka.classpath :as cp]
7-
'[clojure.java.shell :as shell])
6+
'[clojure.java.shell :as shell]
7+
'[clojure.java.io :as io])
88

9-
(defn- ->snake_case [s] (str/replace s \- \_))
10-
11-
(def root-dir "/github/workspace/main/")
12-
(def test-runner-dir "/github/workspace/clojure-test-runner/")
13-
14-
(cp/add-classpath root-dir)
15-
16-
(def practice-root (str root-dir "exercises/practice/"))
17-
(def concept-root (str root-dir "exercises/concept/"))
18-
19-
(defn copy-practice-example! [slug]
20-
(shell/sh "cp"
21-
(str practice-root slug "/.meta/src/example.clj")
22-
(str practice-root slug "/.meta/src/" (->snake_case slug) ".clj")))
9+
(defn clean-path [path]
10+
(if (str/ends-with? path "/")
11+
path
12+
(str path "/")))
2313

24-
(defn replace-practice-example! [slug]
25-
(shell/sh "mv"
26-
(str practice-root slug "/.meta/src/" (->snake_case slug) ".clj")
27-
(str practice-root slug "/src/" (->snake_case slug) ".clj")))
14+
(def root
15+
(clean-path (if (.exists (io/file (str (clean-path (first *command-line-args*)) "config.json")))
16+
(first *command-line-args*)
17+
"/github/workspace/main/")))
2818

29-
(defn practice-pass? [slug]
30-
(let [copy (copy-practice-example! slug)
31-
replace (replace-practice-example! slug)]
32-
(= "pass" ((json/parse-string
33-
(:out (shell/sh (str test-runner-dir "test-runner.clj")
34-
slug
35-
(str practice-root slug "/")
36-
(str practice-root slug "/"))))
37-
"status"))))
19+
(def test-runner-dir
20+
(clean-path (if (.exists (io/file (str (clean-path (first *command-line-args*)) "test-runner.clj")))
21+
(first *command-line-args*)
22+
"/github/workspace/clojure-test-runner/")))
3823

39-
(defn copy-concept-example! [slug]
40-
(shell/sh "cp"
41-
(str concept-root slug "/.meta/exemplar.clj")
42-
(str concept-root slug "/.meta/" (->snake_case slug) ".clj")))
24+
(defn- ->snake_case [s] (str/replace s \- \_))
4325

44-
(defn replace-concept-example! [slug]
45-
(shell/sh "mv"
46-
(str concept-root slug "/.meta/" (->snake_case slug) ".clj")
47-
(str concept-root slug "/src/" (->snake_case slug) ".clj")))
26+
(def practice-exercises
27+
(map #(% "slug")
28+
(-> (str root "config.json")
29+
slurp
30+
json/parse-string
31+
(get "exercises")
32+
(get "practice"))))
4833

49-
(defn concept-pass? [slug]
50-
(let [copy (copy-concept-example! slug)
51-
replace (replace-concept-example! slug)]
34+
(def concept-exercises
35+
(map #(% "slug")
36+
(-> (str root "config.json")
37+
slurp
38+
json/parse-string
39+
(get "exercises")
40+
(get "concept"))))
41+
42+
(defn test-exercise [slug]
43+
(let [practice? (contains? (set practice-exercises) slug)
44+
example (if practice?
45+
(str root "exercises/practice/" slug "/.meta/src/example.clj")
46+
(str root "exercises/concept/" slug "/.meta/exemplar.clj"))
47+
src (if practice?
48+
(str root "exercises/practice/" slug "/src/" (->snake_case slug) ".clj")
49+
(str root "exercises/concept/" slug "/src/" (->snake_case slug) ".clj"))]
50+
(shell/sh "cp" example src)
5251
(= "pass" ((json/parse-string
5352
(:out (shell/sh (str test-runner-dir "test-runner.clj")
5453
slug
55-
(str concept-root slug "/")
56-
(str concept-root slug "/"))))
54+
(str root (if practice? "exercises/practice/" "exercises/concept/") slug "/")
55+
(str root (if practice? "exercises/practice/" "exercises/concept/") slug "/"))))
5756
"status"))))
5857

59-
(def practice-exercises
60-
(map #(% "slug") (((json/parse-string (slurp (str root-dir "config.json"))) "exercises") "practice")))
61-
62-
(def concept-exercises
63-
(map #(% "slug") (((json/parse-string (slurp (str root-dir "config.json"))) "exercises") "concept")))
64-
65-
(defn check-practice-exercises! []
66-
(for [exercise practice-exercises]
67-
{(keyword exercise) (practice-pass? exercise)}))
68-
69-
(defn check-concept-exercises! []
70-
(for [exercise concept-exercises]
71-
{(keyword exercise) (concept-pass? exercise)}))
58+
(defn test-exercises! []
59+
(for [exercise (into practice-exercises concept-exercises)]
60+
{(keyword exercise) (test-exercise exercise)}))
7261

73-
(let [results (into (check-concept-exercises!)
74-
(check-practice-exercises!))
62+
(let [results (test-exercises!)
7563
fails (filter false? results)]
7664
(prn {:tested (count results)
7765
:fails fails})

0 commit comments

Comments
 (0)