Skip to content

Commit c8d072c

Browse files
author
José Valim
committed
Add basic mix test partitioning
1 parent 012ccf9 commit c8d072c

File tree

2 files changed

+94
-18
lines changed

2 files changed

+94
-18
lines changed

lib/mix/lib/mix/tasks/test.ex

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@ defmodule Mix.Tasks.Test do
181181
* `--no-elixir-version-check` - does not check the Elixir version from `mix.exs`
182182
* `--no-start` - does not start applications after compilation
183183
* `--only` - runs only tests that match the filter
184+
* `--partitions` - sets the amount of partitions to split tests in. This option
185+
requires the `MIX_TEST_PARTITION` environment variable to be set. See the
186+
"OS Processes Partitioning" section for more information
184187
* `--preload-modules` - preloads all modules defined in applications
185188
* `--raise` - raises if the test suite failed
186189
* `--seed` - seeds the random number generator used to randomize the order of tests;
@@ -189,12 +192,26 @@ defmodule Mix.Tasks.Test do
189192
Automatically sets `--trace` and `--preload-modules`
190193
* `--stale` - runs only tests which reference modules that changed since the
191194
last time tests were ran with `--stale`. You can read more about this option
192-
in the "Stale" section below
195+
in the "The --stale option" section below
193196
* `--timeout` - sets the timeout for the tests
194197
* `--trace` - runs tests with detailed reporting. Automatically sets `--max-cases` to `1`.
195198
Note that in trace mode test timeouts will be ignored as timeout is set to `:infinity`
196199
197-
See `ExUnit.configure/1` for more information on configuration options.
200+
## Configuration
201+
202+
These configurations can be set in the `def project` section of your `mix.exs:
203+
204+
* `:test_paths` - list of paths containing test files. Defaults to
205+
`["test"]` if the `test` directory exists; otherwise, it defaults to `[]`.
206+
It is expected that all test paths contain a `test_helper.exs` file
207+
208+
* `:test_pattern` - a pattern to load test files. Defaults to `*_test.exs`
209+
210+
* `:warn_test_pattern` - a pattern to match potentially misnamed test files
211+
and display a warning. Defaults to `*_test.ex`
212+
213+
* `:test_coverage` - a set of options to be passed down to the coverage
214+
mechanism
198215
199216
## Filters
200217
@@ -251,20 +268,6 @@ defmodule Mix.Tasks.Test do
251268
If a given line starts a `describe` block, that line filter runs all tests in it.
252269
Otherwise, it runs the closest test on or before the given line number.
253270
254-
## Configuration
255-
256-
* `:test_paths` - list of paths containing test files. Defaults to
257-
`["test"]` if the `test` directory exists; otherwise, it defaults to `[]`.
258-
It is expected that all test paths contain a `test_helper.exs` file
259-
260-
* `:test_pattern` - a pattern to load test files. Defaults to `*_test.exs`
261-
262-
* `:warn_test_pattern` - a pattern to match potentially misnamed test files
263-
and display a warning. Defaults to `*_test.ex`
264-
265-
* `:test_coverage` - a set of options to be passed down to the coverage
266-
mechanism
267-
268271
## Coverage
269272
270273
The `:test_coverage` configuration accepts the following options:
@@ -293,9 +296,35 @@ defmodule Mix.Tasks.Test do
293296
It must return either `nil` or an anonymous function of zero arity that will
294297
be run after the test suite is done.
295298
296-
## "Stale"
299+
## OS Processes Partitioning
300+
301+
While ExUnit supports the ability to run tests concurrently within the same
302+
Elixir instance, it is not always possible to run all tests concurrently. For
303+
example, some tests may rely on global resources.
304+
305+
For this reason, `mix test` all supports partitioning the test files across
306+
different Elixir instances. This is done by setting the `--partitions` option
307+
to an integer, with the number of partitions, and setting the `MIX_TEST_PARTITION`
308+
environment variable to control which test partition that particular instance
309+
is running. This can be also be useful if you want to distribute testing across
310+
multiple machines.
311+
312+
For example, to split a test suite into 4 partitions and run them, you would
313+
use the following commands:
297314
298-
The `--stale` command line option attempts to run only those test files which
315+
MIX_TEST_PARTITION=1 mix test --partitions 4
316+
MIX_TEST_PARTITION=2 mix test --partitions 4
317+
MIX_TEST_PARTITION=3 mix test --partitions 4
318+
MIX_TEST_PARTITION=4 mix test --partitions 4
319+
320+
The test files are sorted and distributed in a round-robin fashion. Note the
321+
partition itself is given as an environment variable so it can be accessed in
322+
configuration files and test scripts. For example, it can be used to setup a
323+
different database instance per partition in `config/test.exs`.
324+
325+
## The --stale option
326+
327+
The `--stale` command line option attempts to run only the test files which
299328
reference modules that have changed since the last time you ran this task with
300329
`--stale`.
301330
@@ -304,6 +333,9 @@ defmodule Mix.Tasks.Test do
304333
references (and any modules those modules reference, recursively) were modified
305334
since the last run with `--stale`. A test file is also marked "stale" if it has
306335
been changed since the last run with `--stale`.
336+
337+
The `--stale` option is extremely useful for software iteration, allowing you to
338+
run only the relevant tests as you perform changes to the codebase.
307339
"""
308340

309341
@switches [
@@ -329,6 +361,7 @@ defmodule Mix.Tasks.Test do
329361
listen_on_stdin: :boolean,
330362
formatter: :keep,
331363
slowest: :integer,
364+
partitions: :integer,
332365
preload_modules: :boolean
333366
]
334367

@@ -441,6 +474,7 @@ defmodule Mix.Tasks.Test do
441474
test_files
442475
|> Mix.Utils.extract_files(test_pattern)
443476
|> filter_to_allowed_files(allowed_files)
477+
|> filter_by_partition(opts)
444478

445479
display_warn_test_pattern(test_files, test_pattern, matched_test_files, warn_test_pattern)
446480

@@ -642,6 +676,33 @@ defmodule Mix.Tasks.Test do
642676
Enum.filter(matched_test_files, &MapSet.member?(allowed_files, Path.expand(&1)))
643677
end
644678

679+
defp filter_by_partition(files, opts) do
680+
if total = opts[:partitions] do
681+
partition = System.get_env("MIX_TEST_PARTITION")
682+
683+
case partition && Integer.parse(partition) do
684+
{partition, ""} when partition in 1..total ->
685+
partition = partition - 1
686+
687+
# We sort the files because Path.wildcard does not guarantee
688+
# ordering, so different OSes could return a different order,
689+
# meaning run across OSes on different partitions could run
690+
# duplicate files.
691+
for {file, index} <- Enum.with_index(Enum.sort(files)),
692+
rem(index, total) == partition,
693+
do: file
694+
695+
_ ->
696+
Mix.raise(
697+
"The MIX_TEST_PARTITION environment variable must be set to an integer between " <>
698+
"1..#{total} when the --partitions option is set, got: #{inspect(partition)}"
699+
)
700+
end
701+
else
702+
files
703+
end
704+
end
705+
645706
defp color_opts(opts) do
646707
case Keyword.fetch(opts, :color) do
647708
{:ok, enabled?} ->

lib/mix/test/mix/tasks/test_test.exs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,21 @@ defmodule Mix.Tasks.TestTest do
268268
end
269269
end
270270

271+
describe "--partitions" do
272+
test "splits tests into partitions" do
273+
in_fixture("test_stale", fn ->
274+
assert mix(["test", "--partitions", "3"], [{"MIX_TEST_PARTITION", "1"}]) =~
275+
"1 test, 0 failures"
276+
277+
assert mix(["test", "--partitions", "3"], [{"MIX_TEST_PARTITION", "2"}]) =~
278+
"1 test, 0 failures"
279+
280+
assert mix(["test", "--partitions", "3"], [{"MIX_TEST_PARTITION", "3"}]) =~
281+
"There are no tests to run"
282+
end)
283+
end
284+
end
285+
271286
describe "logs and errors" do
272287
test "logs test absence for a project with no test paths" do
273288
in_fixture("test_stale", fn ->

0 commit comments

Comments
 (0)