@@ -181,6 +181,9 @@ defmodule Mix.Tasks.Test do
181
181
* `--no-elixir-version-check` - does not check the Elixir version from `mix.exs`
182
182
* `--no-start` - does not start applications after compilation
183
183
* `--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
184
187
* `--preload-modules` - preloads all modules defined in applications
185
188
* `--raise` - raises if the test suite failed
186
189
* `--seed` - seeds the random number generator used to randomize the order of tests;
@@ -189,12 +192,26 @@ defmodule Mix.Tasks.Test do
189
192
Automatically sets `--trace` and `--preload-modules`
190
193
* `--stale` - runs only tests which reference modules that changed since the
191
194
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
193
196
* `--timeout` - sets the timeout for the tests
194
197
* `--trace` - runs tests with detailed reporting. Automatically sets `--max-cases` to `1`.
195
198
Note that in trace mode test timeouts will be ignored as timeout is set to `:infinity`
196
199
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
198
215
199
216
## Filters
200
217
@@ -251,20 +268,6 @@ defmodule Mix.Tasks.Test do
251
268
If a given line starts a `describe` block, that line filter runs all tests in it.
252
269
Otherwise, it runs the closest test on or before the given line number.
253
270
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
-
268
271
## Coverage
269
272
270
273
The `:test_coverage` configuration accepts the following options:
@@ -293,9 +296,35 @@ defmodule Mix.Tasks.Test do
293
296
It must return either `nil` or an anonymous function of zero arity that will
294
297
be run after the test suite is done.
295
298
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` 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 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:
297
314
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
299
328
reference modules that have changed since the last time you ran this task with
300
329
`--stale`.
301
330
@@ -304,6 +333,9 @@ defmodule Mix.Tasks.Test do
304
333
references (and any modules those modules reference, recursively) were modified
305
334
since the last run with `--stale`. A test file is also marked "stale" if it has
306
335
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.
307
339
"""
308
340
309
341
@ switches [
@@ -329,6 +361,7 @@ defmodule Mix.Tasks.Test do
329
361
listen_on_stdin: :boolean ,
330
362
formatter: :keep ,
331
363
slowest: :integer ,
364
+ partitions: :integer ,
332
365
preload_modules: :boolean
333
366
]
334
367
@@ -421,46 +454,45 @@ defmodule Mix.Tasks.Test do
421
454
{ :error , { :already_loaded , :ex_unit } } -> :ok
422
455
end
423
456
424
- # The test helper may change the Mix.shell(), so let's make sure to revert it later
457
+ # The test helper may change the Mix.shell(), so revert it whenever we raise and after suite
425
458
shell = Mix . shell ( )
426
459
427
460
# Configure ExUnit now and then again so the task options override test_helper.exs
428
461
{ ex_unit_opts , allowed_files } = process_ex_unit_opts ( opts )
429
462
ExUnit . configure ( ex_unit_opts )
430
463
431
464
test_paths = project [ :test_paths ] || default_test_paths ( )
432
- Enum . each ( test_paths , & require_test_helper ( & 1 ) )
465
+ Enum . each ( test_paths , & require_test_helper ( shell , & 1 ) )
433
466
ExUnit . configure ( merge_helper_opts ( ex_unit_opts ) )
434
467
435
468
# Finally parse, require and load the files
436
- test_files = parse_files ( files , test_paths )
469
+ test_files = parse_files ( files , shell , test_paths )
437
470
test_pattern = project [ :test_pattern ] || "*_test.exs"
438
471
warn_test_pattern = project [ :warn_test_pattern ] || "*_test.ex"
439
472
440
473
matched_test_files =
441
474
test_files
442
475
|> Mix.Utils . extract_files ( test_pattern )
443
476
|> filter_to_allowed_files ( allowed_files )
477
+ |> filter_by_partition ( shell , opts )
444
478
445
479
display_warn_test_pattern ( test_files , test_pattern , matched_test_files , warn_test_pattern )
446
480
447
- results = CT . require_and_run ( matched_test_files , test_paths , opts )
448
- Mix . shell ( shell )
449
-
450
- case results do
481
+ case CT . require_and_run ( matched_test_files , test_paths , opts ) do
451
482
{ :ok , % { excluded: excluded , failures: failures , total: total } } ->
483
+ Mix . shell ( shell )
452
484
cover && cover . ( )
453
485
454
486
cond do
455
487
failures > 0 and opts [ :raise ] ->
456
- Mix . raise ( "\" mix test\" failed" )
488
+ raise_with_shell ( shell , "\" mix test\" failed" )
457
489
458
490
failures > 0 ->
459
491
System . at_exit ( fn _ -> exit ( { :shutdown , 1 } ) end )
460
492
461
493
excluded == total and Keyword . has_key? ( opts , :only ) ->
462
494
message = "The --only option was given to \" mix test\" but no test was executed"
463
- raise_or_error_at_exit ( message , opts )
495
+ raise_or_error_at_exit ( shell , message , opts )
464
496
465
497
true ->
466
498
:ok
@@ -476,17 +508,22 @@ defmodule Mix.Tasks.Test do
476
508
477
509
true ->
478
510
message = "Paths given to \" mix test\" did not match any directory/file: "
479
- raise_or_error_at_exit ( message <> Enum . join ( files , ", " ) , opts )
511
+ raise_or_error_at_exit ( shell , message <> Enum . join ( files , ", " ) , opts )
480
512
end
481
513
482
514
:ok
483
515
end
484
516
end
485
517
486
- defp raise_or_error_at_exit ( message , opts ) do
518
+ defp raise_with_shell ( shell , message ) do
519
+ Mix . shell ( shell )
520
+ Mix . raise ( message )
521
+ end
522
+
523
+ defp raise_or_error_at_exit ( shell , message , opts ) do
487
524
cond do
488
525
opts [ :raise ] ->
489
- Mix . raise ( message )
526
+ raise_with_shell ( shell , message )
490
527
491
528
Mix.Task . recursing? ( ) ->
492
529
Mix . shell ( ) . info ( message )
@@ -525,10 +562,7 @@ defmodule Mix.Tasks.Test do
525
562
526
563
@ doc false
527
564
def process_ex_unit_opts ( opts ) do
528
- { opts , allowed_files } =
529
- opts
530
- |> manifest_opts ( )
531
- |> failed_opts ( )
565
+ { opts , allowed_files } = manifest_opts ( opts )
532
566
533
567
opts =
534
568
opts
@@ -559,21 +593,21 @@ defmodule Mix.Tasks.Test do
559
593
[ autorun: false ] ++ opts
560
594
end
561
595
562
- defp parse_files ( [ ] , test_paths ) do
596
+ defp parse_files ( [ ] , _shell , test_paths ) do
563
597
test_paths
564
598
end
565
599
566
- defp parse_files ( [ single_file ] , _test_paths ) do
600
+ defp parse_files ( [ single_file ] , _shell , _test_paths ) do
567
601
# Check if the single file path matches test/path/to_test.exs:123. If it does,
568
602
# apply "--only line:123" and trim the trailing :123 part.
569
603
{ single_file , opts } = ExUnit.Filters . parse_path ( single_file )
570
604
ExUnit . configure ( opts )
571
605
[ single_file ]
572
606
end
573
607
574
- defp parse_files ( files , _test_paths ) do
608
+ defp parse_files ( files , shell , _test_paths ) do
575
609
if Enum . any? ( files , & match? ( { _ , [ _ | _ ] } , ExUnit.Filters . parse_path ( & 1 ) ) ) do
576
- Mix . raise ( "Line numbers can only be used when running a single test file" )
610
+ raise_with_shell ( shell , "Line numbers can only be used when running a single test file" )
577
611
else
578
612
files
579
613
end
@@ -620,16 +654,14 @@ defmodule Mix.Tasks.Test do
620
654
621
655
defp manifest_opts ( opts ) do
622
656
manifest_file = Path . join ( Mix.Project . manifest_path ( ) , @ manifest_file_name )
623
- Keyword . put ( opts , :failures_manifest_file , manifest_file )
624
- end
657
+ opts = Keyword . put ( opts , :failures_manifest_file , manifest_file )
625
658
626
- defp failed_opts ( opts ) do
627
659
if opts [ :failed ] do
628
660
if opts [ :stale ] do
629
661
Mix . raise ( "Combining --failed and --stale is not supported." )
630
662
end
631
663
632
- { allowed_files , failed_ids } = ExUnit.Filters . failure_info ( opts [ :failures_manifest_file ] )
664
+ { allowed_files , failed_ids } = ExUnit.Filters . failure_info ( manifest_file )
633
665
{ Keyword . put ( opts , :only_test_ids , failed_ids ) , allowed_files }
634
666
else
635
667
{ opts , nil }
@@ -642,6 +674,34 @@ defmodule Mix.Tasks.Test do
642
674
Enum . filter ( matched_test_files , & MapSet . member? ( allowed_files , Path . expand ( & 1 ) ) )
643
675
end
644
676
677
+ defp filter_by_partition ( files , shell , opts ) do
678
+ if total = opts [ :partitions ] do
679
+ partition = System . get_env ( "MIX_TEST_PARTITION" )
680
+
681
+ case partition && Integer . parse ( partition ) do
682
+ { partition , "" } when partition in 1 .. total ->
683
+ partition = partition - 1
684
+
685
+ # We sort the files because Path.wildcard does not guarantee
686
+ # ordering, so different OSes could return a different order,
687
+ # meaning run across OSes on different partitions could run
688
+ # duplicate files.
689
+ for { file , index } <- Enum . with_index ( Enum . sort ( files ) ) ,
690
+ rem ( index , total ) == partition ,
691
+ do: file
692
+
693
+ _ ->
694
+ raise_with_shell (
695
+ shell ,
696
+ "The MIX_TEST_PARTITION environment variable must be set to an integer between " <>
697
+ "1..#{ total } when the --partitions option is set, got: #{ inspect ( partition ) } "
698
+ )
699
+ end
700
+ else
701
+ files
702
+ end
703
+ end
704
+
645
705
defp color_opts ( opts ) do
646
706
case Keyword . fetch ( opts , :color ) do
647
707
{ :ok , enabled? } ->
@@ -652,13 +712,16 @@ defmodule Mix.Tasks.Test do
652
712
end
653
713
end
654
714
655
- defp require_test_helper ( dir ) do
715
+ defp require_test_helper ( shell , dir ) do
656
716
file = Path . join ( dir , "test_helper.exs" )
657
717
658
718
if File . exists? ( file ) do
659
719
Code . require_file ( file )
660
720
else
661
- Mix . raise ( "Cannot run tests because test helper file #{ inspect ( file ) } does not exist" )
721
+ raise_with_shell (
722
+ shell ,
723
+ "Cannot run tests because test helper file #{ inspect ( file ) } does not exist"
724
+ )
662
725
end
663
726
end
664
727
0 commit comments