Current section

Files

Jump to
mix_test_interactive lib mix_test_interactive settings.ex
Raw

lib/mix_test_interactive/settings.ex

defmodule MixTestInteractive.Settings do
@moduledoc """
Interactive mode settings.
Keeps track of the current settings of `MixTestInteractive.InteractiveMode`, making changes
in response to user commands.
"""
use TypedStruct
alias MixTestInteractive.PatternFilter
alias MixTestInteractive.TestFiles
@default_list_all_files &TestFiles.list/0
typedstruct do
field :excludes, [String.t()], default: []
field :failed?, boolean(), default: false
field :includes, [String.t()], default: []
field :initial_cli_args, [String.t()], default: []
field :list_all_files, (-> [String.t()]), default: @default_list_all_files
field :max_failures, String.t()
field :only, [String.t()], default: []
field :patterns, [String.t()], default: []
field :repeat_count, String.t()
field :seed, String.t()
field :stale?, boolean(), default: false
field :tracing?, boolean(), default: false
field :watching?, boolean(), default: true
end
@doc """
Update settings to run all tests, removing any flags or filter patterns.
"""
@spec all_tests(t()) :: t()
def all_tests(%__MODULE__{} = settings) do
%{settings | failed?: false, patterns: [], stale?: false}
end
@doc """
Update settings to clear any excluded tags.
"""
@spec clear_excludes(t()) :: t()
def clear_excludes(%__MODULE__{} = settings) do
%{settings | excludes: []}
end
@doc """
Update settings to clear any included tags.
"""
@spec clear_includes(t()) :: t()
def clear_includes(%__MODULE__{} = settings) do
%{settings | includes: []}
end
@doc """
Update settings to run with unlimited failures, clearing any specified maximum
number of failures.
"""
@spec clear_max_failures(t()) :: t()
def clear_max_failures(%__MODULE__{} = settings) do
%{settings | max_failures: nil}
end
@doc """
Update settings to clear any "only" tags.
"""
@spec clear_only(t()) :: t()
def clear_only(%__MODULE__{} = settings) do
%{settings | only: []}
end
@doc """
Update settings to run tests only once, clearing any repeat-until-failure
count.
"""
@spec clear_repeat_count(t()) :: t()
def clear_repeat_count(%__MODULE__{} = settings) do
%{settings | repeat_count: nil}
end
@doc """
Update settings to run tests with a random seed, clearing any specified seed.
"""
@spec clear_seed(t()) :: t()
def clear_seed(%__MODULE__{} = settings) do
%{settings | seed: nil}
end
@doc """
Assemble command-line arguments to pass to `mix test`.
Includes arguments originally passed to `mix test.interactive` when it was started
as well as arguments based on the current interactive mode settings.
"""
@spec cli_args(t()) :: {:ok, [String.t()]} | {:error, :no_matching_files}
def cli_args(%__MODULE__{} = settings) do
with {:ok, args} <- args_from_settings(settings) do
{:ok, settings.initial_cli_args ++ args}
end
end
@doc false
def list_files_with(%__MODULE__{} = settings, list_fn) do
%{settings | list_all_files: list_fn}
end
@doc """
Update settings to only run failing tests.
Corresponds to `mix test --failed`.
"""
@spec only_failed(t()) :: t()
def only_failed(%__MODULE__{} = settings) do
settings
|> all_tests()
|> Map.put(:failed?, true)
end
@doc """
Provide a list of file-name filter patterns.
Only test filenames matching one or more patterns will be run.
"""
@spec only_patterns(t(), [String.t()]) :: t()
def only_patterns(%__MODULE__{} = settings, patterns) do
settings
|> all_tests()
|> Map.put(:patterns, patterns)
end
@doc """
Update settings to only run "stale" tests.
Corresponds to `mix test --stale`.
"""
@spec only_stale(t()) :: t()
def only_stale(%__MODULE__{} = settings) do
settings
|> all_tests()
|> Map.put(:stale?, true)
end
@doc """
Return a text summary of the current interactive mode settings.
"""
@spec summary(t()) :: String.t()
def summary(%__MODULE__{} = settings) do
run_summary =
cond do
settings.failed? ->
"Ran only failed tests"
settings.stale? ->
"Ran only stale tests"
!Enum.empty?(settings.patterns) ->
"Ran all test files matching #{Enum.join(settings.patterns, ", ")}"
true ->
"Ran all tests"
end
with_seed =
case settings.seed do
nil -> run_summary
seed -> run_summary <> " with seed: #{seed}"
end
with_seed
|> append_tag_filters(settings)
|> append_max_failures(settings)
|> append_repeat_count(settings)
|> append_tracing(settings)
end
defp append_max_failures(summary, %__MODULE__{max_failures: nil} = _settings) do
summary
end
defp append_max_failures(summary, %__MODULE__{} = settings) do
summary <> "\nMax failures: #{settings.max_failures}"
end
defp append_repeat_count(summary, %__MODULE__{repeat_count: nil}), do: summary
defp append_repeat_count(summary, %__MODULE__{} = settings) do
summary <> "\nRepeat until failure: #{settings.repeat_count}"
end
defp append_tag_filters(summary, %__MODULE__{} = settings) do
[
summary,
tag_filters("Excluding tags", settings.excludes),
tag_filters("Including tags", settings.includes),
tag_filters("Only tags", settings.only)
]
|> Enum.reject(&is_nil/1)
|> Enum.join("\n")
end
defp append_tracing(summary, %__MODULE__{tracing?: false}), do: summary
defp append_tracing(summary, %__MODULE__{tracing?: true}) do
summary <> "\nTracing: ON"
end
defp tag_filters(_label, []), do: nil
defp tag_filters(label, tags) do
label <> ": " <> inspect(tags)
end
@doc """
Toggle test tracing on or off.
"""
@spec toggle_tracing(t()) :: t()
def toggle_tracing(%__MODULE__{} = settings) do
%{settings | tracing?: !settings.tracing?}
end
@doc """
Toggle file-watching mode on or off.
"""
@spec toggle_watch_mode(t()) :: t()
def toggle_watch_mode(%__MODULE__{} = settings) do
%{settings | watching?: !settings.watching?}
end
@doc """
Exclude tests with the specified tags.
Corresponds to `mix test --exclude <tag1> --exclude <tag2> ...`.
"""
@spec with_excludes(t(), [String.t()]) :: t()
def with_excludes(%__MODULE__{} = settings, excludes) do
%{settings | excludes: excludes}
end
@doc """
Include tests with the specified tags.
Corresponds to `mix test --include <tag1> --include <tag2> ...`.
"""
@spec with_includes(t(), [String.t()]) :: t()
def with_includes(%__MODULE__{} = settings, includes) do
%{settings | includes: includes}
end
@doc """
Stop running tests after a maximum number of failures.
Corresponds to `mix test --max-failures <max>`.
"""
@spec with_max_failures(t(), String.t()) :: t()
def with_max_failures(%__MODULE__{} = settings, max) do
%{settings | max_failures: max}
end
@doc """
Run only the tests with the specified tags.
Corresponds to `mix test --only <tag1> --only <tag2> ...`.
"""
@spec with_only(t(), [String.t()]) :: t()
def with_only(%__MODULE__{} = settings, only) do
%{settings | only: only}
end
@doc """
Update settings to run tests <count> times until failure.
Corresponds to `mix test --repeat-until-failure <count>`.
"""
@spec with_repeat_count(t(), String.t()) :: t()
def with_repeat_count(%__MODULE__{} = settings, count) do
%{settings | repeat_count: count}
end
@doc """
Update settings to run tests with a specific seed.
Corresponds to `mix test --seed <seed>`.
"""
@spec with_seed(t(), String.t()) :: t()
def with_seed(%__MODULE__{} = settings, seed) do
%{settings | seed: seed}
end
defp args_from_settings(%__MODULE{} = settings) do
with {:ok, files} <- files_from_patterns(settings) do
{:ok, opts_from_settings(settings) ++ files}
end
end
defp files_from_patterns(%__MODULE__{patterns: []} = _settings) do
{:ok, []}
end
defp files_from_patterns(%__MODULE__{patterns: patterns} = settings) do
case PatternFilter.matches(settings.list_all_files.(), patterns) do
[] -> {:error, :no_matching_files}
files -> {:ok, files}
end
end
defp opts_from_settings(%__MODULE__{} = settings) do
settings
|> Map.from_struct()
|> Enum.sort()
|> Enum.flat_map(&opts_from_single_setting/1)
end
defp opts_from_single_setting({:excludes, excludes}) do
Enum.flat_map(excludes, &["--exclude", &1])
end
defp opts_from_single_setting({:failed?, false}), do: []
defp opts_from_single_setting({:failed?, true}), do: ["--failed"]
defp opts_from_single_setting({:includes, includes}) do
Enum.flat_map(includes, &["--include", &1])
end
defp opts_from_single_setting({:max_failures, nil}), do: []
defp opts_from_single_setting({:max_failures, max}), do: ["--max-failures", max]
defp opts_from_single_setting({:only, only}) do
Enum.flat_map(only, &["--only", &1])
end
defp opts_from_single_setting({:repeat_count, nil}), do: []
defp opts_from_single_setting({:repeat_count, count}), do: ["--repeat-until-failure", count]
defp opts_from_single_setting({:seed, nil}), do: []
defp opts_from_single_setting({:seed, seed}), do: ["--seed", seed]
defp opts_from_single_setting({:stale?, false}), do: []
defp opts_from_single_setting({:stale?, true}), do: ["--stale"]
defp opts_from_single_setting({:tracing?, false}), do: []
defp opts_from_single_setting({:tracing?, true}), do: ["--trace"]
defp opts_from_single_setting({_key, _value}), do: []
end