@@ -228,10 +228,30 @@ defmodule Mix.Tasks.Test do
228228 `["test"]` if the `test` directory exists, otherwise, it defaults to `[]`.
229229 It is expected that all test paths contain a `test_helper.exs` file
230230
231- * `:test_pattern` - a pattern to load test files. Defaults to `*_test.exs`
231+ * `:test_pattern` - a pattern to find potential test files.
232+ Defaults to `*.{ex,exs}`.
232233
233- * `:warn_test_pattern` - a pattern to match potentially misnamed test files
234- and display a warning. Defaults to `*_test.ex`
234+ In Elixir versions earlier than 1.19.0, this option defaulted to `*_test.exs`,
235+ but to allow better warnings for misnamed test files, it since matches any
236+ Elixir file and expects those to be filtered by `:test_load_filters` and
237+ `:test_ignore_filters`.
238+
239+ * `:test_load_filters` - a list of files, regular expressions or one-arity
240+ functions to restrict which files matched by the `:test_pattern` are loaded.
241+ Defaults to `[&String.ends_with?(&1, "_test.exs")]`. Paths are relative to
242+ the project root and separated by `/`, even on Windows.
243+
244+ * `:test_ignore_filters` - a list of files, regular expressions or one-arity
245+ functions to restrict which files matched by the `:test_pattern`, but not loaded
246+ by `:test_load_filters`, trigger a warning for a potentially misnamed test file.
247+
248+ Mix ignores files ending in `_helper.exs` by default, as well as any file
249+ included in the project's `:elixirc_paths`. This ensures that any helper
250+ or test support files are not triggering a warning.
251+
252+ Any extra filters configured in the project are appended to the defaults.
253+ Warnings can be disabled by setting this option to `[fn _ -> true end]`.
254+ Paths are relative to the project root and separated by `/`, even on Windows.
235255
236256 ## Coloring
237257
@@ -595,20 +615,38 @@ defmodule Mix.Tasks.Test do
595615
596616 # Prepare and extract all files to require and run
597617 test_paths = project [ :test_paths ] || default_test_paths ( )
598- test_pattern = project [ :test_pattern ] || "*_test.exs"
599- warn_test_pattern = project [ :warn_test_pattern ] || "*_test.ex"
618+ test_pattern = project [ :test_pattern ] || "*.{ex,exs}"
619+
620+ # Warn about deprecated warn configuration
621+ if project [ :warn_test_pattern ] do
622+ Mix . shell ( ) . info ( """
623+ warning: the `:warn_test_pattern` configuration is deprecated and will be ignored. \
624+ Use `:test_load_filters` and `:test_ignore_filters` instead.
625+ """ )
626+ end
600627
601628 { test_files , test_opts } =
602629 if files != [ ] , do: ExUnit.Filters . parse_paths ( files ) , else: { test_paths , [ ] }
603630
604- unfiltered_test_files = Mix.Utils . extract_files ( test_files , test_pattern )
631+ # get a list of all files in the test folders, which we filter by the test_load_filters
632+ { potential_test_files , directly_included_test_files } = extract_files ( test_files , test_pattern )
633+
634+ { load_files , _ignored_files , warn_files } =
635+ classify_test_files ( potential_test_files , project )
636+
637+ # ensure that files given as direct argument to mix test are loaded,
638+ # even if the test_load_filters don't match
639+ load_files =
640+ if files != [ ] ,
641+ do: Enum . uniq ( load_files ++ directly_included_test_files ) ,
642+ else: load_files
605643
606644 matched_test_files =
607- unfiltered_test_files
645+ load_files
608646 |> filter_to_allowed_files ( allowed_files )
609647 |> filter_by_partition ( shell , partitions )
610648
611- display_warn_test_pattern ( test_files , test_pattern , unfiltered_test_files , warn_test_pattern )
649+ warn_files != [ ] && warn_misnamed_test_files ( warn_files )
612650
613651 try do
614652 Enum . each ( test_paths , & require_test_helper ( shell , & 1 ) )
@@ -660,6 +698,33 @@ defmodule Mix.Tasks.Test do
660698 end
661699 end
662700
701+ # similar to Mix.Utils.extract_files/2, but returns a list of directly included test files,
702+ # that should be not filtered by the test_load_filters, e.g.
703+ # mix test test/some_file.exs
704+ defp extract_files ( paths , pattern ) do
705+ { files , directly_included } =
706+ for path <- paths , reduce: { [ ] , [ ] } do
707+ { acc , directly_included } ->
708+ case :elixir_utils . read_file_type ( path ) do
709+ { :ok , :directory } ->
710+ { [ Path . wildcard ( "#{ path } /**/#{ pattern } " ) | acc ] , directly_included }
711+
712+ { :ok , :regular } ->
713+ { [ path | acc ] , [ path | directly_included ] }
714+
715+ _ ->
716+ { acc , directly_included }
717+ end
718+ end
719+
720+ files =
721+ files
722+ |> List . flatten ( )
723+ |> Enum . uniq ( )
724+
725+ { files , directly_included }
726+ end
727+
663728 defp raise_with_shell ( shell , message ) do
664729 Mix . shell ( shell )
665730 Mix . raise ( message )
@@ -679,14 +744,64 @@ defmodule Mix.Tasks.Test do
679744 end
680745 end
681746
682- defp display_warn_test_pattern ( test_files , test_pattern , matched_test_files , warn_test_pattern ) do
683- files = Mix.Utils . extract_files ( test_files , warn_test_pattern ) -- matched_test_files
747+ defp classify_test_files ( potential_test_files , project ) do
748+ test_load_filters = project [ :test_load_filters ] || [ & String . ends_with? ( & 1 , "_test.exs" ) ]
749+ elixirc_paths = project [ :elixirc_paths ] || [ ]
750+
751+ # ignore any _helper.exs files and files that are compiled (test support files)
752+ test_ignore_filters =
753+ [
754+ & String . ends_with? ( & 1 , "_helper.exs" ) ,
755+ fn file -> Enum . any? ( elixirc_paths , & String . starts_with? ( file , & 1 ) ) end
756+ ] ++ Keyword . get ( project , :test_ignore_filters , [ ] )
757+
758+ { to_load , to_ignore , to_warn } =
759+ for file <- potential_test_files , reduce: { [ ] , [ ] , [ ] } do
760+ { to_load , to_ignore , to_warn } ->
761+ cond do
762+ any_file_matches? ( file , test_load_filters ) ->
763+ { [ file | to_load ] , to_ignore , to_warn }
764+
765+ any_file_matches? ( file , test_ignore_filters ) ->
766+ { to_load , [ file | to_ignore ] , to_warn }
767+
768+ true ->
769+ { to_load , to_ignore , [ file | to_warn ] }
770+ end
771+ end
684772
685- for file <- files do
686- Mix . shell ( ) . info (
687- "warning: #{ file } does not match #{ inspect ( test_pattern ) } and won't be loaded"
688- )
689- end
773+ # get the files back in the original order
774+ { Enum . reverse ( to_load ) , Enum . reverse ( to_ignore ) , Enum . reverse ( to_warn ) }
775+ end
776+
777+ defp any_file_matches? ( file , filters ) do
778+ Enum . any? ( filters , fn filter ->
779+ case filter do
780+ regex when is_struct ( regex , Regex ) ->
781+ Regex . match? ( regex , file )
782+
783+ binary when is_binary ( binary ) ->
784+ file == binary
785+
786+ fun when is_function ( fun , 1 ) ->
787+ fun . ( file )
788+ end
789+ end )
790+ end
791+
792+ defp warn_misnamed_test_files ( ignored ) do
793+ Mix . shell ( ) . info ( """
794+ warning: the following files do not match any of the configured `:test_load_filters` / `:test_ignore_filters`:
795+
796+ #{ Enum . join ( ignored , "\n " ) }
797+
798+ This might indicate a typo in a test file name (for example, using "foo_tests.exs" instead of "foo_test.exs").
799+
800+ You can adjust which files trigger this warning by configuring the `:test_ignore_filters` option in your
801+ Mix project's configuration. To disable the warning entirely, set that option to false.
802+
803+ For more information, run `mix help test`.
804+ """ )
690805 end
691806
692807 @ option_keys [
0 commit comments