Skip to content

Commit 9446c96

Browse files
camsaulappleby
andauthored
Support test partitioning (#28)
* Support test partitioning * Test fix * Update README.md Co-authored-by: appleby <[email protected]> --------- Co-authored-by: appleby <[email protected]>
1 parent 5920218 commit 9446c96

File tree

3 files changed

+112
-2
lines changed

3 files changed

+112
-2
lines changed

README.md

+23-1
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,33 @@ they live in is loaded; this may be affected by `:only` options passed to the te
202202

203203
Return values of methods are ignored; they are done purely for side effects.
204204

205+
## Partitioning tests
206+
207+
You can divide a test suite into multiple partitions using the `:partition/total` and `:partition/index` keys. This is
208+
an easy way to speed up CI by diving large test suites into multiple jobs.
209+
210+
```
211+
clj -X:test '{:partition/total 10, :partition/index 8}'
212+
...
213+
Running tests in partition 9 of 10 (575 tests of 5753)...
214+
Finding tests took 46.6 s.
215+
Running 575 tests
216+
...
217+
```
218+
219+
`:partition/index` is zero-based, e.g. if you have ten partitions (`:partiton/total 10`) then the first partition is `0` and
220+
the last is `9`.
221+
222+
Tests are partitioned at the `deftest` level after all tests are found the usual way -- all namespaces that would be
223+
loaded if you were running the entire test suite are still loaded. Partitions are split as evenly as possible, but
224+
tests are guaranteed to be split deterministically into exactly the number of partitions you asked for.
225+
226+
205227
## Additional options
206228

207229
All other options are passed directly to [Eftest](https://github.com/weavejester/eftest); refer to its documentation
208230
for more information.
209231

210232
```
211-
clj -X:test :fail-fast? true
233+
clj -X:test '{:fail-fast? true}'
212234
```

src/mb/hawk/core.clj

+44-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
(:require
33
[clojure.java.classpath :as classpath]
44
[clojure.java.io :as io]
5+
[clojure.math :as math]
56
[clojure.pprint :as pprint]
67
[clojure.set :as set]
78
[clojure.string :as str]
@@ -120,14 +121,56 @@
120121
[_nil options]
121122
(find-tests (classpath/system-classpath) options))
122123

124+
(defn- partition-all-into-n-partitions
125+
"Split sequence `xs` into `num-partitions` as equally as possible. Guaranteed to return `num-partitions`. This custom
126+
function is used instead of [[partition-all]] or whatever because we want to make sure every partition gets tests,
127+
even with weird combinations like 4 tests with 3 partitions or 29 tests with 10 partitions."
128+
[num-partitions xs]
129+
{:post [(= (count %) num-partitions)]}
130+
;; make sure the partitioning is deterministic -- `xs` should always come back in the same order but we should sort
131+
;; just to be safe.
132+
(let [xs (sort-by str xs)
133+
partition-size (/ (count xs) num-partitions)]
134+
(into []
135+
(comp (map-indexed (fn [i x]
136+
[(long (math/floor (/ i partition-size))) x]))
137+
(partition-by first)
138+
(map (fn [partition]
139+
(map second partition))))
140+
xs)))
141+
142+
(defn- partition-tests [tests {num-partitions :partition/total, partition-index :partition/index, :as _options}]
143+
(if (or num-partitions partition-index)
144+
(do
145+
(assert (and num-partitions partition-index)
146+
":partition/total and :partition/index must be set together")
147+
(assert (pos-int? num-partitions)
148+
"Invalid :partition/total - must be a positive integer")
149+
(assert (<= num-partitions (count tests))
150+
"Invalid :partition/total - cannot have more partitions than number of tests")
151+
(assert (int? partition-index)
152+
"Invalid :partition/index - must be an integer")
153+
(assert (<= 0 partition-index (dec num-partitions))
154+
(format "Invalid :partition/index - must be between 0 and %d" (dec num-partitions)))
155+
(let [partitions (partition-all-into-n-partitions num-partitions tests)
156+
partition (nth partitions partition-index)]
157+
(printf "Running tests in partition %d of %d (%d tests of %d)...\n"
158+
(inc partition-index)
159+
num-partitions
160+
(count partition)
161+
(count tests))
162+
partition))
163+
tests))
164+
123165
(defn find-tests-with-options
124166
"Find tests using the options map as passed to `clojure -X`."
125167
[{:keys [only], :as options}]
126168
(println "Running tests with options" (pr-str options))
127169
(when only
128170
(println "Running tests in" (pr-str only)))
129171
(let [start-time-ms (System/currentTimeMillis)
130-
tests (find-tests only options)]
172+
tests (-> (find-tests only options)
173+
(partition-tests options))]
131174
(printf "Finding tests took %s.\n" (u/format-milliseconds (- (System/currentTimeMillis) start-time-ms)))
132175
(println "Running" (count tests) "tests")
133176
tests))

test/mb/hawk/core_test.clj

+45
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,48 @@
5858
{:exclude-tags [:exclude-this-test]}
5959
{:exclude-tags #{:exclude-this-test}}
6060
{:exclude-tags [:exclude-this-test :another/tag]}))
61+
62+
(deftest ^:parallel partition-tests-test
63+
(are [i expected] (= expected
64+
(#'hawk/partition-tests
65+
(range 4)
66+
{:partition/index i, :partition/total 3}))
67+
0 [0 1]
68+
1 [2]
69+
2 [3])
70+
(are [i expected] (= expected
71+
(#'hawk/partition-tests
72+
(range 5)
73+
{:partition/index i, :partition/total 3}))
74+
0 [0 1]
75+
1 [2 3]
76+
2 [4]))
77+
78+
(deftest ^:parallel partition-tests-determinism-test
79+
(testing "partitioning should be deterministic even if tests come back in a non-deterministic order for some reason"
80+
(are [i expected] (= expected
81+
(#'hawk/partition-tests
82+
(shuffle (map #(format "%02d" %) (range 26)))
83+
{:partition/index i, :partition/total 10}))
84+
0 ["00" "01" "02"]
85+
1 ["03" "04" "05"]
86+
2 ["06" "07"]
87+
3 ["08" "09" "10"]
88+
4 ["11" "12"]
89+
5 ["13" "14" "15"]
90+
6 ["16" "17" "18"]
91+
7 ["19" "20"]
92+
8 ["21" "22" "23"]
93+
9 ["24" "25"])))
94+
95+
(deftest ^:parallel partition-test
96+
(are [index expected] (= expected
97+
(hawk/find-tests-with-options {:only `[find-tests-test
98+
exclude-tags-test
99+
partition-tests-test
100+
partition-test]
101+
:partition/index index
102+
:partition/total 3}))
103+
0 [#'exclude-tags-test #'find-tests-test]
104+
1 [#'partition-test]
105+
2 [#'partition-tests-test]))

0 commit comments

Comments
 (0)