Skip to content

Commit 615ea82

Browse files
plexusalysbrooks
authored andcommitted
Polish up parallelization: clearer config, docs, vestigial code
Clean up the changes around parallelization, make opting-in from test types explicit, allow config on testable and metadata level, simplify future handling.
1 parent fbfd541 commit 615ea82

File tree

11 files changed

+166
-104
lines changed

11 files changed

+166
-104
lines changed

doc/03_configuration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ Here's an example test configuration with a single test suite:
2929
:kaocha.plugin.randomize/seed 950716166
3030
:kaocha.plugin.randomize/randomize? true
3131
:kaocha.plugin.profiling/count 3
32-
:kaocha.plugin.profiling/profiling? true}
32+
:kaocha.plugin.profiling/profiling? true
33+
:kaocha/parallize? false}
3334
```
3435

3536
Writing a full test configuration by hand is tedious, which is why in

doc/11_parallelization.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# 11. Parallelization
2+
3+
Parallelization is an optional Kaocha feature, where it distributes your test
4+
workload across multiple threads, to make better use of multiple CPU cores.
5+
6+
This is still a relatively new feature, one that has a chance of interfering in
7+
various ways with plugins, custom hooks, or the particulars of test setups that
8+
people have "in the wild". We very much welcome feedback and improvements.
9+
Please be mindful and respectful of the maintainers though and report issues
10+
clearly, preferably with a link to a git repository containing a minimal
11+
reproduction of the issue.
12+
13+
## Configuration and high-level behavior
14+
15+
You can enable parallelization either via the `--parallelize` command line flag,
16+
or by setting `:parallelize? true` in your `tests.edn`. This is assuming that
17+
you are using the `#kaocha/v1` reader literal to provide normalization. The
18+
canonical configuration key is `:kaocha/parallelize?`.
19+
20+
Kaocha looks at your tests as a hierarchy, at the top level there are your test
21+
suites (e.g. unit vs intergration, or clj vs cljs). These contain groups of
22+
tests (their children), e.g. one for each namespace, and these in turn contain
23+
multiple tests, e.g. one for each test var.
24+
25+
Setting `:parallelize true?` at the top-level configuration, or using the
26+
command line flag, will run any suites you have in parallel, as well making
27+
parallelization the default for any type of testable that has children. So say
28+
for instance you have a suite of type `clojure.test`, then multiple test
29+
namespaces will be run in parallel, and individual test vars within those
30+
namespaces will also be started in parallel.
31+
32+
Test type implementations need to opt-in to parallelization. For instance,
33+
Clojure is multi-threaded, but ClojureScript (running on a JavaScript runtime)
34+
is not, so thre is little benefit in trying to parallelize ClojureScript tests.
35+
So even with parallelization on, `kaocha.cljs` or `kaocha.cljs2` tests will
36+
still run in series.
37+
38+
## Fine-grained opting in or out
39+
40+
Using the command line flag or setting `:parallelize? true` at the top-level of
41+
tests.edn will cause any testable that is parallelizable to be run in parallel.
42+
If you want more fine-grained control you can configure specific test suites to
43+
be parallelized, or set metadata on namespaces to opt in or out of
44+
parallelization.
45+
46+
```clj
47+
#kaocha/v1
48+
{:tests [{:id :unit, :parallelize? true}])
49+
```
50+
51+
This will cause all namespaces in the unit test suite to be run in parallel, but
52+
since the default (top-level config) is not set, vars within those namespaces
53+
will not run in parallel. But you can again opt-in at that specific level,
54+
through metadata.
55+
56+
```clj
57+
(ns ^{:kaocha/parallelize? true} my-test
58+
(:require [clojure.test :as t]))
59+
60+
...
61+
```
62+
63+
Conversely you can opt-out of parallelization on the test suite or test
64+
namespace level by setting `:kaocha/parallelize? false`.
65+
66+
## Caveats
67+
68+
When you start running your tests in parallel you will likely notice one or two
69+
things. The first is that your output looks all messed up. Before you might see
70+
something like `[(....)(.......)][(.......)]`, whereas now it looks more like
71+
`[[(..(..).....(..)...]....)]`. This will be even more pronounced if you are for
72+
instance using the documentation reporter. Output from multiple tests gets
73+
interleaved, causing a garbled mess.
74+
75+
The default dots reporter is probably the most usable reporter right now.
76+
77+
The second thing you might notice is that you are getting failures where before
78+
you got none. This likely indicates that your tests themselves are not thread
79+
safe. They may for instance be dealing with shared mutable state.
80+
81+
You will have to examine your code carefully. Starting out with a more piecemeal
82+
opting in might be helpful to narrow things down.
83+
84+
It is also possible that you encounters failures caused by Kaocha itself. In
85+
that case please report them on our issue tracker.

src/kaocha/api.clj

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,6 @@
123123
" To investigate, check the :test-paths and :ns-patterns keys in tests.edn.")))
124124
(throw+ {:kaocha/early-exit 0}))
125125

126-
(when (:parallel config)
127-
(output/warn (str "Parallelization enabled. This feature is in "
128-
"beta If you encounter errors, try "
129-
"running with the feature disabled and "
130-
"consider filing an issue.")))
131126
(when (find-ns 'matcher-combinators.core)
132127
(require 'kaocha.matcher-combinators))
133128

@@ -142,8 +137,7 @@
142137
;; don't know where in the process we've
143138
;; been interrupted, output capturing may
144139
;; still be in effect.
145-
(System/setOut
146-
orig-out)
140+
(System/setOut orig-out)
147141
(System/setErr
148142
orig-err)
149143
(binding [history/*history* history]
@@ -160,13 +154,13 @@
160154
on-exit)
161155
(let [test-plan (plugin/run-hook :kaocha.hooks/pre-run test-plan)]
162156
(binding [testable/*test-plan* test-plan]
163-
(let [test-plan-tests (:kaocha.test-plan/tests test-plan)
164-
result-tests (testable/run-testables test-plan-tests test-plan)
165-
result (plugin/run-hook :kaocha.hooks/post-run
166-
(-> test-plan
167-
(dissoc :kaocha.test-plan/tests)
168-
(assoc :kaocha.result/tests result-tests)))]
169-
(assert (= (count test-plan-tests) (count (:kaocha.result/tests result))))
157+
(let [result-tests (testable/run-testables-parent test-plan test-plan)
158+
result (plugin/run-hook :kaocha.hooks/post-run
159+
(-> test-plan
160+
(dissoc :kaocha.test-plan/tests)
161+
(assoc :kaocha.result/tests result-tests)))]
162+
(assert (= (count (:kaocha.test-plan/tests test-plan))
163+
(count (:kaocha.result/tests result))))
170164
(-> result
171165
result/testable-totals
172166
result/totals->clojure-test-summary

src/kaocha/config.clj

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@
5555
(rename-key :skip :kaocha.filter/skip)
5656
(rename-key :focus :kaocha.filter/focus)
5757
(rename-key :skip-meta :kaocha.filter/skip-meta)
58-
(rename-key :focus-meta :kaocha.filter/focus-meta))]
58+
(rename-key :focus-meta :kaocha.filter/focus-meta)
59+
(rename-key :parallelize? :kaocha/parallelize?))]
5960
(as-> m $
6061
(merge-config (first (:kaocha/tests (default-config))) $)
6162
(merge {:kaocha.testable/desc (str (name (:kaocha.testable/id $))
@@ -82,7 +83,8 @@
8283
randomize?
8384
capture-output?
8485
watch?
85-
bindings]} config
86+
bindings
87+
parallelize?]} config
8688
tests (some->> tests (mapv normalize-test-suite))]
8789
(cond-> {}
8890
tests (assoc :kaocha/tests (vary-meta tests assoc :replace true))
@@ -95,6 +97,7 @@
9597
(some? watch?) (assoc :kaocha/watch? watch?)
9698
(some? randomize?) (assoc :kaocha.plugin.randomize/randomize? randomize?)
9799
(some? capture-output?) (assoc :kaocha.plugin.capture-output/capture-output? capture-output?)
100+
(some? parallelize?) (assoc :kaocha/parallelize? parallelize?)
98101
:-> (merge (dissoc config :tests :plugins :reporter :color? :fail-fast? :watch? :randomize?)))))
99102

100103
(defmethod aero/reader 'kaocha [_opts _tag value]
@@ -195,17 +198,16 @@
195198
config
196199
(read-config nil opts))))
197200

198-
199201
(defn apply-cli-opts [config options]
200202
(cond-> config
201-
(some? (:fail-fast options)) (assoc :kaocha/fail-fast? (:fail-fast options))
202-
(:reporter options) (assoc :kaocha/reporter (:reporter options))
203-
(:watch options) (assoc :kaocha/watch? (:watch options))
204-
(some? (:color options)) (assoc :kaocha/color? (:color options))
205-
(some? (:diff-style options)) (assoc :kaocha/diff-style (:diff-style options))
206-
(:plugin options) (update :kaocha/plugins #(distinct (concat % (:plugin options))))
207-
(some? (:parallel options)) (assoc :parallel (:parallel options))
208-
true (assoc :kaocha/cli-options options)))
203+
(some? (:fail-fast options)) (assoc :kaocha/fail-fast? (:fail-fast options))
204+
(:reporter options) (assoc :kaocha/reporter (:reporter options))
205+
(:watch options) (assoc :kaocha/watch? (:watch options))
206+
(some? (:color options)) (assoc :kaocha/color? (:color options))
207+
(some? (:diff-style options)) (assoc :kaocha/diff-style (:diff-style options))
208+
(:plugin options) (update :kaocha/plugins #(distinct (concat % (:plugin options))))
209+
(some? (:parallelize options)) (assoc :kaocha/parallelize? (:parallelize options))
210+
true (assoc :kaocha/cli-options options)))
209211

210212
(defn apply-cli-args [config args]
211213
(if (seq args)

src/kaocha/runner.clj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
[nil "--[no-]fail-fast" "Stop testing after the first failure."]
3535
[nil "--[no-]color" "Enable/disable ANSI color codes in output. Defaults to true."]
3636
[nil "--[no-]watch" "Watch filesystem for changes and re-run tests."]
37-
[nil "--[no-]parallel" "Run tests in parallel. Warning: This feature is beta."]
37+
[nil "--[no-]parallelize" "Run tests in parallel."]
3838
[nil "--reporter SYMBOL" "Change the test reporter, can be specified multiple times."
3939
:parse-fn (fn [s]
4040
(let [sym (symbol s)]

src/kaocha/test_suite.clj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44

55
(defn run [testable test-plan]
66
(t/do-report {:type :begin-test-suite})
7-
(let [results (testable/run-testables (:kaocha.test-plan/tests testable) test-plan)
7+
(let [results (testable/run-testables-parent testable test-plan)
88
testable (-> testable
99
(dissoc :kaocha.test-plan/tests)
1010
(assoc :kaocha.result/tests results))]
1111
(t/do-report {:type :end-test-suite
1212
:kaocha/testable testable})
13-
testable))
13+
testable))

src/kaocha/testable.clj

Lines changed: 36 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -49,22 +49,6 @@
4949
(try-require (symbol (namespace type))))
5050
(try-require (symbol (name type)))))
5151

52-
(defn- retry-assert-spec [type testable n]
53-
(let [result (try (assert-spec type testable) (catch Exception _e false))]
54-
(if (or result (<= n 1)) result
55-
(retry-assert-spec type testable (dec n))) ;otherwise, retry
56-
))
57-
58-
(defn deref-recur [testables]
59-
(cond (future? testables) (deref testables)
60-
(vector? testables) (doall (mapv deref-recur testables))
61-
(seq? testables) (deref-recur (into [] (doall testables)))
62-
(contains? testables :kaocha.test-plan/tests)
63-
(update testables :kaocha.test-plan/tests deref-recur)
64-
(contains? testables :kaocha.result/tests)
65-
(update testables :kaocha.result/tests deref-recur)
66-
:else testables))
67-
6852
(defn- load-type+validate
6953
"Try to load a testable type, and validate it both to be a valid generic testable, and a valid instance given the type.
7054
@@ -162,9 +146,9 @@
162146
result))))
163147

164148
(spec/fdef run
165-
:args (spec/cat :testable :kaocha.test-plan/testable
166-
:test-plan :kaocha/test-plan)
167-
:ret :kaocha.result/testable)
149+
:args (spec/cat :testable :kaocha.test-plan/testable
150+
:test-plan :kaocha/test-plan)
151+
:ret :kaocha.result/testable)
168152

169153
(defn load-testables
170154
"Load a collection of testables, returning a test-plan collection"
@@ -240,18 +224,15 @@
240224

241225
(defn try-run-testable [test test-plan n]
242226
(let [result (try (run-testable test test-plan) (catch Exception _e false))]
243-
(if (or result (> n 1)) result ;success or last try, return
244-
(try-run-testable test test-plan (dec n))) ;otherwise retry
245-
))
246-
227+
(if (or result (> n 1))
228+
;; success or last try, return
229+
result
230+
;; otherwise retry
231+
(try-run-testable test test-plan (dec n)))))
247232

248233
(defn run-testables-serial
249234
"Run a collection of testables, returning a result collection."
250235
[testables test-plan]
251-
(doall testables)
252-
#_(print "run-testables got a collection of size" (count testables)
253-
" the first of which is "
254-
(:kaocha.testable/type (first testables)))
255236
(let [load-error? (some ::load-error testables)]
256237
(loop [result []
257238
[test & testables] testables]
@@ -261,7 +242,7 @@
261242
(assoc ::skip true))
262243
r (run-testable test test-plan)]
263244
(if (or (and *fail-fast?* (result/failed? r)) (::skip-remaining? r))
264-
(reduce into result [[r] testables])
245+
(into (conj result r) testables)
265246
(recur (conj result r) testables)))
266247
result))))
267248

@@ -272,41 +253,36 @@
272253
:group-name (.getName (.getThreadGroup thread))}))
273254

274255
(defn run-testables-parallel
275-
"Run a collection of testables, returning a result collection."
256+
"Run a collection of testables in parallel, returning a result collection."
276257
[testables test-plan]
277-
(doall testables)
278258
(let [load-error? (some ::load-error testables)
279-
types (set (:parallel-children-exclude *config*))
280-
suites (:parallel-suites-exclude *config*)
281-
futures (map #(do
259+
futures (doall
260+
(map (fn [t]
282261
(future
283-
(binding [*config*
284-
(cond-> *config*
285-
(contains? types (:kaocha.testable/type %)) (dissoc :parallel)
286-
(and (hierarchy/suite? %) (contains? suites (:kaocha.testable/desc %))) (dissoc :parallel)
287-
true (update :levels (fn [x] (if (nil? x) 1 (inc x)))))]
288-
(run-testable (assoc % ::thread (current-thread-info) ) test-plan))))
289-
testables)]
290-
(comment (loop [result [] ;(ArrayBlockingQueue. 1024)
291-
[test & testables] testables]
292-
(if test
293-
(let [test (cond-> test
294-
(and load-error? (not (::load-error test)))
295-
(assoc ::skip true))
296-
r (run-testable test test-plan)]
297-
(if (or (and *fail-fast?* (result/failed? r)) (::skip-remaining? r))
298-
;(reduce put-return result [[r] testables])
299-
(reduce into result [[r] testables])
300-
;(recur (doto result (.put r)) testables)
301-
(recur (conj result r) testables)))
302-
result)))
303-
(deref-recur futures)))
262+
(run-testable (assoc t ::thread-info (current-thread-info)) test-plan)))
263+
testables))]
264+
(doall (map deref futures))))
304265

305266
(defn run-testables
267+
"Original run-testables, left for backwards compatibility, and still usable for
268+
test types that don't want to opt-in to parallelization. Generally
269+
implementations should move to [[run-testables-parent]]."
306270
[testables test-plan]
307-
(if (:parallel *config*)
308-
(doall (run-testables-parallel testables test-plan))
309-
(run-testables-serial testables test-plan)))
271+
(run-testables-serial testables test-plan))
272+
273+
(defn run-testables-parent
274+
"Test type implementations should call this in their [[-run]] method, rather
275+
than [[run-testables]], so we can inspect the parent and parent metadata to
276+
decide if the children should get parallelized."
277+
[parent test-plan]
278+
(let [testables (:kaocha.test-plan/tests parent)]
279+
(if (or (true? (:kaocha/parallelize? (::meta parent))) ; explicit opt-in via metadata
280+
(and (:kaocha/parallelize? test-plan) ; enable parallelization in top-level config
281+
(or (::parallelizable? parent) ; test type has opted in, children are considered parallelizable
282+
(:kaocha/parallelize? parent)) ; or we're at the top level, suites are parallelizable. Can also be used as an explicit override/opt-in
283+
(not (false? (:kaocha/parallelize? (::meta parent)))))) ; explicit opt-out via metadata
284+
(run-testables-parallel testables test-plan)
285+
(run-testables-serial testables test-plan))))
310286

311287
(defn test-seq [testable]
312288
(cond->> (mapcat test-seq (remove ::skip (or (:kaocha/tests testable)
@@ -319,12 +295,12 @@
319295

320296
(defn test-seq-with-skipped
321297
[testable]
322-
"Create a seq of all tests, including any skipped tests.
298+
"Create a seq of all tests, including any skipped tests.
323299
324300
Typically you want to look at `test-seq` instead."
325301
(cond->> (mapcat test-seq (or (:kaocha/tests testable)
326-
(:kaocha.test-plan/tests testable)
327-
(:kaocha.result/tests testable)))
302+
(:kaocha.test-plan/tests testable)
303+
(:kaocha.result/tests testable)))
328304
;; When calling test-seq on the top level test-plan/result, don't include
329305
;; the outer map. When running on an actual testable, do include it.
330306
(:kaocha.testable/id testable)

src/kaocha/type/clojure/test.clj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
(defmethod testable/-load :kaocha.type/clojure.test [testable]
1313
(-> testable
14+
(assoc :kaocha.testable/parallelizable? true)
1415
(load/load-test-namespaces type.ns/->testable)
1516
(testable/add-desc "clojure.test")))
1617

src/kaocha/type/ns.clj

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,19 @@
88
[kaocha.type :as type]))
99

1010
(defn ->testable [ns-name]
11-
{:kaocha.testable/type :kaocha.type/ns
12-
:kaocha.testable/id (keyword (str ns-name))
13-
:kaocha.testable/desc (str ns-name)
14-
:kaocha.ns/name ns-name})
11+
{:kaocha.testable/type :kaocha.type/ns
12+
:kaocha.testable/id (keyword (str ns-name))
13+
:kaocha.testable/desc (str ns-name)
14+
:kaocha.testable/parallelizable? true
15+
:kaocha.ns/name ns-name})
1516

1617
(defn run-tests [testable test-plan fixture-fn]
1718
;; It's not guaranteed the the fixture-fn returns the result of calling the
1819
;; tests function, so we need to put it in a box for reference.
19-
(let [result (atom (:kaocha.test-plan/tests testable))]
20-
(fixture-fn #(swap! result testable/run-testables test-plan))
20+
(let [result (promise)]
21+
(fixture-fn
22+
(fn []
23+
(deliver result (testable/run-testables-parent testable test-plan))))
2124
@result))
2225

2326
(defmethod testable/-load :kaocha.type/ns [testable]

0 commit comments

Comments
 (0)