Skip to content

Commit 09ebf2b

Browse files
committed
(mix test) add test_load_filters and test_warn_filters
When using the default project configuration, mix test would only warn about files ending in `_test.ex`. The only mistake this warns about is forgetting the `s` of `.exs`. In a work project we noticed (after more than a year) that we had multiple test files that did not match the test pattern, preventing them from running in mix test locally and CI. Because we have many other tests, nobody noticed this. If CI passes, all is good, right? Because there is no easy way to evaluate glob patterns in Elixir without touching the filesystem, I decided to deprecate the old `warn_test_pattern` configuration and instead add two new configurations: `test_load_filters` and `test_warn_filters` Now, by changing the default of `test_pattern` to `*.{ex,exs}`, we can load all potential test files once and then match their paths to the patterns. The `test_load_filters` is used to filter the files that are loaded by mix test. This defaults to the regex equivalent of "*_test.exs". The `test_warn_filters` is used to filter the files that we warn about if they are not loaded. By default, we ignore any file ending in `_helper.exs`, which will prevent the default test_helper.exs from generating a warning and also provides a simple way to name other files that might be required explicitly in tests. We also default to ignore any files that start with a configured elixirc_path, which are compiled often test support files. For projects with an existing `warn_test_pattern` configuration, a deprecation warning is logged. The warnings can be disabled by setting `test_warn_filters` to a falsy value. Projects with an existing custom `test_pattern` should check if their pattern conflicts with the new `test_load_filters` and adjust their configuration accordingly. It is also possible to keep the old `test_pattern` and configure the `test_load_filters` to accept any file, for example by configuring it to `[fn _ -> true end]`. In that case, the `test_warn_filters` don't have an effect, as any potential test file is also loaded.
1 parent b75cccc commit 09ebf2b

File tree

14 files changed

+156
-18
lines changed

14 files changed

+156
-18
lines changed

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

Lines changed: 101 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -228,10 +228,33 @@ 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_warn_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 `[~r/.*_test\\.exs$/]`
242+
243+
* `:test_warn_filters` - a list of files, regular expressions or one-arity
244+
functions to restrict which files matched by the `:test_pattern`, but not loaded
245+
by `:test_load_filters`, trigger a warning for a potentially misnamed test file.
246+
247+
Defaults to:
248+
249+
```elixir
250+
[
251+
~r/.*_helper\.exs$/,
252+
fn file -> Enum.any?(elixirc_paths(Mix.env()), &String.starts_with?(file, &1)) end
253+
]
254+
```
255+
256+
This ensures that any helper or test support files are not triggering a warning.
257+
Warnings can be disabled by setting this option to `false` or `nil`.
235258
236259
## Coloring
237260
@@ -595,20 +618,31 @@ defmodule Mix.Tasks.Test do
595618

596619
# Prepare and extract all files to require and run
597620
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"
621+
test_pattern = project[:test_pattern] || "*.{ex,exs}"
622+
623+
# Warn about deprecated warn configuration
624+
if project[:warn_test_pattern] do
625+
Mix.shell().info("""
626+
warning: the `:warn_test_pattern` configuration is deprecated and will be ignored. \
627+
Use `:test_load_filters` and `:test_warn_filters` instead.
628+
""")
629+
end
600630

601631
{test_files, test_opts} =
602632
if files != [], do: ExUnit.Filters.parse_paths(files), else: {test_paths, []}
603633

604-
unfiltered_test_files = Mix.Utils.extract_files(test_files, test_pattern)
634+
# get a list of all files in the test folders, which we filter by the test_load_filters
635+
potential_test_files = Mix.Utils.extract_files(test_files, test_pattern)
636+
637+
{unfiltered_test_files, _ignored_files, warn_files} =
638+
classify_test_files(potential_test_files, project)
605639

606640
matched_test_files =
607641
unfiltered_test_files
608642
|> filter_to_allowed_files(allowed_files)
609643
|> filter_by_partition(shell, partitions)
610644

611-
display_warn_test_pattern(test_files, test_pattern, unfiltered_test_files, warn_test_pattern)
645+
warn_files != [] && warn_misnamed_test_files(warn_files)
612646

613647
try do
614648
Enum.each(test_paths, &require_test_helper(shell, &1))
@@ -679,14 +713,67 @@ defmodule Mix.Tasks.Test do
679713
end
680714
end
681715

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
716+
defp classify_test_files(potential_test_files, project) do
717+
test_load_filters = project[:test_load_filters] || [~r/.*_test\.exs$/]
718+
elixirc_paths = project[:elixirc_paths] || []
684719

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
720+
# ignore any _helper.exs files and files that are compiled (test support files)
721+
test_warn_filters =
722+
Keyword.get_lazy(project, :test_warn_filters, fn ->
723+
[
724+
~r/.*_helper\.exs$/,
725+
fn file -> Enum.any?(elixirc_paths, &String.starts_with?(file, &1)) end
726+
]
727+
end)
728+
729+
{to_load, to_ignore, to_warn} =
730+
for file <- potential_test_files, reduce: {[], [], []} do
731+
{to_load, to_ignore, to_warn} ->
732+
cond do
733+
any_file_matches?(file, test_load_filters) ->
734+
{[file | to_load], to_ignore, to_warn}
735+
736+
any_file_matches?(file, test_warn_filters) ->
737+
{to_load, [file | to_ignore], to_warn}
738+
739+
# don't warn if test_warn_filters is explicitly set to nil / false
740+
!!test_warn_filters ->
741+
{to_load, to_ignore, [file | to_warn]}
742+
end
743+
end
744+
745+
# get the files back in the original order
746+
{Enum.reverse(to_load), Enum.reverse(to_ignore), Enum.reverse(to_warn)}
747+
end
748+
749+
defp any_file_matches?(file, filters) do
750+
Enum.any?(filters, fn filter ->
751+
case filter do
752+
regex when is_struct(regex, Regex) ->
753+
Regex.match?(regex, file)
754+
755+
binary when is_binary(binary) ->
756+
file == binary
757+
758+
fun when is_function(fun, 1) ->
759+
fun.(file)
760+
end
761+
end)
762+
end
763+
764+
defp warn_misnamed_test_files(ignored) do
765+
Mix.shell().info("""
766+
warning: the following files do not match any of the configured `:test_load_filters` / `:test_warn_filters`:
767+
768+
#{Enum.join(ignored, "\n")}
769+
770+
This might indicate a typo in a test file name (for example, using "foo_tests.exs" instead of "foo_test.exs").
771+
772+
You can adjust which files trigger this warning by configuring the `:test_warn_filters` option in your
773+
Mix project's configuration. To disable the warning entirely, set that option to false.
774+
775+
For more information, run `mix help test`.
776+
""")
690777
end
691778

692779
@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
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule TestWarn.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :test_warn,
7+
version: "0.0.1",
8+
test_load_filters: [~r/.*_tests\.exs/],
9+
test_warn_filters: [
10+
"test/test_helper.exs",
11+
~r/ignored_regex/,
12+
fn file -> file == "test/ignored_file.exs" end
13+
]
14+
]
15+
end
16+
end

lib/mix/test/fixtures/test_warn/test/a_missing.exs

Whitespace-only changes.

lib/mix/test/fixtures/test_warn/test/a_tests.ex

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
defmodule ATests do
2+
use ExUnit.Case
3+
4+
test "dummy" do
5+
assert true
6+
end
7+
end

lib/mix/test/fixtures/test_warn/test/ignored_file.exs

Whitespace-only changes.

lib/mix/test/fixtures/test_warn/test/ignored_regex.exs

Whitespace-only changes.

lib/mix/test/fixtures/test_warn/test/other_file.txt

Whitespace-only changes.

0 commit comments

Comments
 (0)