Skip to content

Commit dc258bb

Browse files
committed
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 b947c62 commit dc258bb

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
@@ -114,11 +114,6 @@
114114
" To investigate, check the :test-paths and :ns-patterns keys in tests.edn.")))
115115
(throw+ {:kaocha/early-exit 0}))
116116

117-
(when (:parallel config)
118-
(output/warn (str "Parallelization enabled. This feature is in "
119-
"beta If you encounter errors, try "
120-
"running with the feature disabled and "
121-
"consider filing an issue.")))
122117
(when (find-ns 'matcher-combinators.core)
123118
(require 'kaocha.matcher-combinators))
124119

@@ -133,8 +128,7 @@
133128
;; don't know where in the process we've
134129
;; been interrupted, output capturing may
135130
;; still be in effect.
136-
(System/setOut
137-
orig-out)
131+
(System/setOut orig-out)
138132
(binding [history/*history* history]
139133
(t/do-report (history/clojure-test-summary)))
140134
(catch Throwable t
@@ -149,13 +143,13 @@
149143
on-exit)
150144
(let [test-plan (plugin/run-hook :kaocha.hooks/pre-run test-plan)]
151145
(binding [testable/*test-plan* test-plan]
152-
(let [test-plan-tests (:kaocha.test-plan/tests test-plan)
153-
result-tests (testable/run-testables test-plan-tests test-plan)
154-
result (plugin/run-hook :kaocha.hooks/post-run
155-
(-> test-plan
156-
(dissoc :kaocha.test-plan/tests)
157-
(assoc :kaocha.result/tests result-tests)))]
158-
(assert (= (count test-plan-tests) (count (:kaocha.result/tests result))))
146+
(let [result-tests (testable/run-testables-parent test-plan test-plan)
147+
result (plugin/run-hook :kaocha.hooks/post-run
148+
(-> test-plan
149+
(dissoc :kaocha.test-plan/tests)
150+
(assoc :kaocha.result/tests result-tests)))]
151+
(assert (= (count (:kaocha.test-plan/tests test-plan))
152+
(count (:kaocha.result/tests result))))
159153
(-> result
160154
result/testable-totals
161155
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
@@ -35,7 +35,7 @@
3535
[nil "--[no-]fail-fast" "Stop testing after the first failure."]
3636
[nil "--[no-]color" "Enable/disable ANSI color codes in output. Defaults to true."]
3737
[nil "--[no-]watch" "Watch filesystem for changes and re-run tests."]
38-
[nil "--[no-]parallel" "Run tests in parallel. Warning: This feature is beta."]
38+
[nil "--[no-]parallelize" "Run tests in parallel."]
3939
[nil "--reporter SYMBOL" "Change the test reporter, can be specified multiple times."
4040
:parse-fn (fn [s]
4141
(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
@@ -50,22 +50,6 @@
5050
(try-require (symbol (namespace type))))
5151
(try-require (symbol (name type)))))
5252

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

165149
(spec/fdef run
166-
:args (spec/cat :testable :kaocha.test-plan/testable
167-
:test-plan :kaocha/test-plan)
168-
:ret :kaocha.result/testable)
150+
:args (spec/cat :testable :kaocha.test-plan/testable
151+
:test-plan :kaocha/test-plan)
152+
:ret :kaocha.result/testable)
169153

170154
(defn load-testables
171155
"Load a collection of testables, returning a test-plan collection"
@@ -241,18 +225,15 @@
241225

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

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

@@ -273,41 +254,36 @@
273254
:group-name (.getName (.getThreadGroup thread))}))
274255

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

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

312288
(defn test-seq [testable]
313289
(cond->> (mapcat test-seq (remove ::skip (or (:kaocha/tests testable)
@@ -320,12 +296,12 @@
320296

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

src/kaocha/type/clojure/test.clj

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

1414
(defmethod testable/-load :kaocha.type/clojure.test [testable]
1515
(-> testable
16+
(assoc :kaocha.testable/parallelizable? true)
1617
(load/load-test-namespaces type.ns/->testable)
1718
(testable/add-desc "clojure.test")))
1819

src/kaocha/type/ns.clj

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

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

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

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

0 commit comments

Comments
 (0)