Skip to content

Commit a4cb71c

Browse files
authored
Add test_load_filters and test_warn_filters to mix test (#14036)
1 parent 6dc0d6f commit a4cb71c

File tree

6 files changed

+244
-19
lines changed

6 files changed

+244
-19
lines changed

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

Lines changed: 130 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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 [

lib/mix/test/fixtures/test_failed/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule TestFailed.MixProject do
55
[
66
app: :test_only_failures,
77
version: "0.0.1",
8-
test_pattern: "*_test_failed.exs"
8+
test_load_filters: [~r/.*_test_failed\.exs/]
99
]
1010
end
1111
end

lib/mix/test/fixtures/test_stale/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule TestStale.MixProject do
55
[
66
app: :test_stale,
77
version: "0.0.1",
8-
test_pattern: "*_test_stale.exs"
8+
test_load_filters: [fn file -> String.ends_with?(file, "_test_stale.exs") end]
99
]
1010
end
1111
end

lib/mix/test/fixtures/umbrella_test/apps/bar/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule Bar.MixProject do
77
version: "0.1.0",
88
# Choose something besides *_test.exs so that these test files don't
99
# get accidentally swept up into the actual Mix test suite.
10-
test_pattern: "*_tests.exs",
10+
test_load_filters: [~r/.*_tests\.exs/],
1111
test_coverage: [ignore_modules: [Bar, ~r/Ignore/]],
1212
aliases: [mytask: fn _ -> Mix.shell().info("bar_running") end]
1313
]

lib/mix/test/fixtures/umbrella_test/apps/foo/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule Foo.MixProject do
77
version: "0.1.0",
88
# Choose something besides *_test.exs so that these test files don't
99
# get accidentally swept up into the actual Mix test suite.
10-
test_pattern: "*_tests.exs",
10+
test_load_filters: [~r/.*_tests\.exs/],
1111
aliases: [mytask: fn _ -> Mix.shell().info("foo_running") end]
1212
]
1313
end

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

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,116 @@ defmodule Mix.Tasks.TestTest do
624624
end
625625
end
626626

627+
describe "test_load_filters and test_ignore_filters" do
628+
test "warns for files that are not loaded and don't match test_ignore_filters" do
629+
in_tmp("test_warn", fn ->
630+
File.write!("mix.exs", """
631+
defmodule TestWarn.MixProject do
632+
use Mix.Project
633+
634+
def project do
635+
[
636+
app: :test_warn,
637+
version: "0.0.1",
638+
test_load_filters: [~r/.*_tests\.exs/],
639+
test_ignore_filters: [
640+
"test/test_helper.exs",
641+
~r/ignored_regex/,
642+
fn file -> file == "test/ignored_file.exs" end
643+
]
644+
]
645+
end
646+
end
647+
""")
648+
649+
File.mkdir!("test")
650+
651+
File.write!("test/a_tests.exs", """
652+
defmodule ATests do
653+
use ExUnit.Case
654+
655+
test "dummy" do
656+
assert true
657+
end
658+
end
659+
""")
660+
661+
File.write!("test/test_helper.exs", "ExUnit.start()")
662+
File.touch("test/a_missing.exs")
663+
File.touch("test/a_tests.ex")
664+
File.touch("test/ignored_file.exs")
665+
File.touch("test/ignored_regex.exs")
666+
File.write!("test/other_file.txt", "this is not a test file")
667+
668+
output = mix(["test"])
669+
670+
# This test relies on the files present in the test_warn fixture.
671+
#
672+
# We test that we don't warn about a_tests.exs, as it already matches the load pattern.
673+
# Similarly, we ignore the empty but present ignored_file.exs and ignored_regex.exs.
674+
# other_file.txt does not match the test_pattern and is ignored from the beginning.
675+
#
676+
# Therefore, we expect to get a warning for a_missing.exs and a_tests.ex.
677+
assert output =~ """
678+
the following files do not match any of the configured `:test_load_filters` / `:test_ignore_filters`:
679+
680+
test/a_missing.exs
681+
test/a_tests.ex
682+
683+
This might indicate a typo\
684+
"""
685+
686+
# the dummy test ran successfully
687+
assert output =~ "1 test, 0 failures"
688+
end)
689+
end
690+
691+
test "does not warn when test_ignore_filters are disabled" do
692+
in_tmp("test_warn", fn ->
693+
File.write!("mix.exs", """
694+
defmodule TestWarn.MixProject do
695+
use Mix.Project
696+
697+
def project do
698+
[
699+
app: :test_warn,
700+
version: "0.0.1",
701+
test_load_filters: [~r/.*_tests\.exs/],
702+
test_ignore_filters: [fn _ -> true end]
703+
]
704+
end
705+
end
706+
""")
707+
708+
File.mkdir!("test")
709+
710+
File.write!("test/a_tests.exs", """
711+
defmodule ATests do
712+
use ExUnit.Case
713+
714+
test "dummy" do
715+
assert true
716+
end
717+
end
718+
""")
719+
720+
File.write!("test/test_helper.exs", "ExUnit.start()")
721+
File.touch("test/a_missing.exs")
722+
File.touch("test/a_tests.ex")
723+
File.touch("test/ignored_file.exs")
724+
File.touch("test/ignored_regex.exs")
725+
File.write!("test/other_file.txt", "this is not a test file")
726+
727+
output = mix(["test"])
728+
729+
refute output =~ "the following files do not match"
730+
731+
# the dummy test ran successfully
732+
assert output =~ "1 test, 0 failures"
733+
end)
734+
end
735+
end
736+
627737
defp receive_until_match(port, expected, acc) do
628738
receive do
629739
{^port, {:data, output}} ->

0 commit comments

Comments
 (0)