Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add repeat until failure command #118

Merged
merged 2 commits into from
Sep 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ which tests should be run with a few keystrokes.
It allows you to easily switch between running all tests, stale tests, or failed
tests. Or, you can run only the tests whose filenames contain a substring. You
can also control which tags are included or excluded, modify the maximum number
of failures allowed, specify the test seed to use, and toggle tracing on and
off. Includes an optional "watch mode" which runs tests after every file change.
of failures allowed, repeat the test suite until a failure occurs, specify the
test seed to use, and toggle tracing on and off. Includes an optional "watch
mode" which runs tests after every file change.

## Installation

Expand Down Expand Up @@ -118,6 +119,10 @@ will run.
e.g. `test/my_project/my_test.exs`, `test/my_project/my_test.exs:12:24` or
`my`.
- `q`: Exit the program. (Can also use `Ctrl-D`.)
- `r <count>`: (Elixir 1.17.0 and later) Run tests up to <count> times until a
failure occurs (equivalent to the `--repeat-until-failure` option of `mix
test`).
- `r`: (Elixir 1.17.0 and later) Clear the "repeat-until-failure" count.
- `s`: Run only test files that reference modules that have changed since the
last run (equivalent to the `--stale` option of `mix test`).
- `t`: Turn test tracing on or off (equivalent to the `--trace` option of `mix
Expand Down
10 changes: 7 additions & 3 deletions lib/mix/tasks/test/interactive.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ defmodule Mix.Tasks.Test.Interactive do
`mix test.interactive` allows you to easily switch between running all tests,
stale tests, or failed tests. Or, you can run only the tests whose filenames
contain a substring. You can also control which tags are included or excluded,
modify the maximum number of failures allowed, specify the test seed to use,
and toggle tracing on and off. Includes an optional "watch mode" which runs
tests after every file change.
modify the maximum number of failures allowed, repeat the test suite until a
failure occurs, specify the test seed to use, and toggle tracing on and off.
Includes an optional "watch mode" which runs tests after every file change.

## Usage

Expand Down Expand Up @@ -94,6 +94,10 @@ defmodule Mix.Tasks.Test.Interactive do
e.g. `test/my_project/my_test.exs`, `test/my_project/my_test.exs:12:24` or
`my`.
- `q`: Exit the program. (Can also use `Ctrl-D`.)
- `r <count>`: (Elixir 1.17.0 and later) Run tests up to <count> times until a
failure occurs (equivalent to the `--repeat-until-failure` option of `mix
test`).
- `r`: (Elixir 1.17.0 and later) Clear the "repeat-until-failure" count.
- `s`: Run only test files that reference modules that have changed since the
last run (equivalent to the `--stale` option of `mix test`).
- `t`: Turn test tracing on or off (equivalent to the `--trace` option of `mix
Expand Down
30 changes: 30 additions & 0 deletions lib/mix_test_interactive/command/repeat_until_failure.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
defmodule MixTestInteractive.Command.RepeatUntilFailure do
@moduledoc """
Specify or clear the number of repetitions for running until failure.

Runs the tests repeatedly until failure or until the specified number of runs.
If not provided, the count is cleared and the tests will run just once as
usual.

Corresponds to `mix test --repeat-until-failure <count>`.

This option is only available in `mix test` in Elixir 1.17.0 and later.
"""
use MixTestInteractive.Command, command: "r", desc: "set or clear the repeat-until-failure count"

alias MixTestInteractive.Command
alias MixTestInteractive.Settings

@impl Command
def name, do: "r [<count>]"

@impl Command
def run([], %Settings{} = settings) do
{:ok, Settings.clear_repeat_count(settings)}
end

@impl Command
def run([count], %Settings{} = settings) do
{:ok, Settings.with_repeat_count(settings, count)}
end
end
2 changes: 2 additions & 0 deletions lib/mix_test_interactive/command_line_parser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ defmodule MixTestInteractive.CommandLineParser do
{includes, mix_test_opts} = Keyword.pop_values(mix_test_opts, :include)
{only, mix_test_opts} = Keyword.pop_values(mix_test_opts, :only)
{max_failures, mix_test_opts} = Keyword.pop(mix_test_opts, :max_failures)
{repeat_count, mix_test_opts} = Keyword.pop(mix_test_opts, :repeat_until_failure)
{seed, mix_test_opts} = Keyword.pop(mix_test_opts, :seed)
{stale?, mix_test_opts} = Keyword.pop(mix_test_opts, :stale, false)
{trace?, mix_test_opts} = Keyword.pop(mix_test_opts, :trace, false)
Expand All @@ -181,6 +182,7 @@ defmodule MixTestInteractive.CommandLineParser do
max_failures: max_failures && to_string(max_failures),
only: only,
patterns: patterns,
repeat_count: repeat_count && to_string(repeat_count),
seed: seed && to_string(seed),
stale?: no_patterns? && !failed? && stale?,
tracing?: trace?,
Expand Down
2 changes: 2 additions & 0 deletions lib/mix_test_interactive/command_processor.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule MixTestInteractive.CommandProcessor do
alias MixTestInteractive.Command.Only
alias MixTestInteractive.Command.Pattern
alias MixTestInteractive.Command.Quit
alias MixTestInteractive.Command.RepeatUntilFailure
alias MixTestInteractive.Command.RunTests
alias MixTestInteractive.Command.Seed
alias MixTestInteractive.Command.Stale
Expand All @@ -32,6 +33,7 @@ defmodule MixTestInteractive.CommandProcessor do
Only,
Pattern,
Quit,
RepeatUntilFailure,
RunTests,
Seed,
Stale,
Expand Down
3 changes: 2 additions & 1 deletion lib/mix_test_interactive/interactive_mode.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ defmodule MixTestInteractive.InteractiveMode do
alias MixTestInteractive.CommandProcessor
alias MixTestInteractive.Config
alias MixTestInteractive.Runner
alias MixTestInteractive.RunSummary
alias MixTestInteractive.Settings

@type option :: {:config, Config.t()} | {:name | String.t()}
Expand Down Expand Up @@ -116,7 +117,7 @@ defmodule MixTestInteractive.InteractiveMode do
IO.puts("")

settings
|> Settings.summary()
|> RunSummary.from_settings()
|> IO.puts()
end

Expand Down
71 changes: 71 additions & 0 deletions lib/mix_test_interactive/run_summary.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
defmodule MixTestInteractive.RunSummary do
@moduledoc false
alias MixTestInteractive.Settings

@doc """
Return a text summary of the current interactive mode settings.
"""
@spec from_settings(Settings.t()) :: String.t()
def from_settings(%Settings{} = settings) do
[&base_summary/1, &all_tag_filters/1, &max_failures/1, &repeat_count/1, &seed/1, &tracing/1]
|> Enum.flat_map(fn fun -> List.wrap(fun.(settings)) end)
|> Enum.join("\n")
end

defp all_tag_filters(%Settings{} = settings) do
Enum.reject(
[
tag_filters("Excluding tags", settings.excludes),
tag_filters("Including tags", settings.includes),
tag_filters("Only tags", settings.only)
],
&is_nil/1
)
end

defp base_summary(%Settings{} = settings) do
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
end

defp max_failures(%Settings{max_failures: nil}), do: nil

defp max_failures(%Settings{} = settings) do
"Max failures: #{settings.max_failures}"
end

defp repeat_count(%Settings{repeat_count: nil}), do: nil

defp repeat_count(%Settings{} = settings) do
"Repeat until failure: #{settings.repeat_count}"
end

def seed(%Settings{seed: nil}), do: nil

def seed(%Settings{} = settings) do
"Seed: #{settings.seed}"
end

defp tracing(%Settings{tracing?: false}), do: nil

defp tracing(%Settings{}) do
"Tracing: ON"
end

defp tag_filters(_label, []), do: nil

defp tag_filters(label, tags) do
label <> ": " <> inspect(tags)
end
end
30 changes: 30 additions & 0 deletions lib/mix_test_interactive/settings.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ defmodule MixTestInteractive.Settings do
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
Expand Down Expand Up @@ -69,6 +70,15 @@ defmodule MixTestInteractive.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.
"""
Expand Down Expand Up @@ -160,6 +170,7 @@ defmodule MixTestInteractive.Settings do
with_seed
|> append_tag_filters(settings)
|> append_max_failures(settings)
|> append_repeat_count(settings)
|> append_tracing(settings)
end

Expand All @@ -171,6 +182,12 @@ defmodule MixTestInteractive.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,
Expand Down Expand Up @@ -250,6 +267,16 @@ defmodule MixTestInteractive.Settings 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.

Expand Down Expand Up @@ -302,6 +329,9 @@ defmodule MixTestInteractive.Settings 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]

Expand Down
6 changes: 6 additions & 0 deletions test/mix_test_interactive/command_line_parser_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,12 @@ defmodule MixTestInteractive.CommandLineParserTest do
assert settings.initial_cli_args == ["--color", "--raise"]
end

test "extracts repeat-until-failure from arguments" do
{:ok, %{settings: settings}} = CommandLineParser.parse(["--color", "--repeat-until-failure", "1000", "--raise"])
assert settings.repeat_count == "1000"
assert settings.initial_cli_args == ["--color", "--raise"]
end

test "extracts seed from arguments" do
{:ok, %{settings: settings}} = CommandLineParser.parse(["--color", "--seed", "5432", "--raise"])
assert settings.seed == "5432"
Expand Down
14 changes: 14 additions & 0 deletions test/mix_test_interactive/command_processor_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ defmodule MixTestInteractive.CommandProcessorTest do
assert {:ok, ^expected} = process_command("p second", first_config)
end

test "r <count> sets the repeat until failure count" do
settings = %Settings{}
expected = Settings.with_repeat_count(settings, "4200")

assert {:ok, ^expected} = process_command("r 4200", settings)
end

test "r with no count clears the repeat until failure count" do
{:ok, settings} = process_command("r 1000", %Settings{})
expected = Settings.clear_repeat_count(settings)

assert {:ok, ^expected} = process_command("r", settings)
end

test "s runs only stale tests" do
settings = %Settings{}
expected = Settings.only_stale(settings)
Expand Down
10 changes: 10 additions & 0 deletions test/mix_test_interactive/end_to_end_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ defmodule MixTestInteractive.EndToEndTest do
assert_ran_tests()
end

test "repeat until failure workflow", %{pid: pid} do
assert_ran_tests()

assert :ok = InteractiveMode.process_command(pid, "r 1000")
assert_ran_tests(["--repeat-until-failure", "1000"])

assert :ok = InteractiveMode.process_command(pid, "r")
assert_ran_tests()
end

test "seed workflow", %{pid: pid} do
assert_ran_tests()

Expand Down
Loading