Skip to content

Add mix test --dry-run flag #14499

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
30 changes: 21 additions & 9 deletions lib/ex_unit/lib/ex_unit/runner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,27 @@ defmodule ExUnit.Runner do
config = configure(opts, manager, self(), stats_pid)
:erlang.system_flag(:backtrace_depth, Keyword.fetch!(opts, :stacktrace_depth))

start_time = System.monotonic_time()
EM.suite_started(config.manager, opts)

modules_to_restore =
if Keyword.fetch!(opts, :repeat_until_failure) > 0, do: {[], []}, else: nil

modules_to_restore =
if not opts[:dry_run] do
do_run(config, modules_to_restore, opts, load_us)
else
nil
end

stats = ExUnit.RunnerStats.stats(stats_pid)
EM.stop(config.manager)
after_suite_callbacks = Application.fetch_env!(:ex_unit, :after_suite)
Enum.each(after_suite_callbacks, fn callback -> callback.(stats) end)
{stats, modules_to_restore}
Comment on lines +50 to +61
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whatyouhide So, I tried to move the flag down the stack as far as possible. I still need to write tests, but does this look more in the right direction? 🙏

end

defp do_run(config, modules_to_restore, opts, load_us) do
start_time = System.monotonic_time()
EM.suite_started(config.manager, opts)

{async_stop_time, modules_to_restore} = async_loop(config, %{}, false, modules_to_restore)
stop_time = System.monotonic_time()

Expand All @@ -65,11 +80,7 @@ defmodule ExUnit.Runner do
times_us = %{async: async_us, load: load_us, run: run_us}
EM.suite_finished(config.manager, times_us)

stats = ExUnit.RunnerStats.stats(stats_pid)
EM.stop(config.manager)
after_suite_callbacks = Application.fetch_env!(:ex_unit, :after_suite)
Enum.each(after_suite_callbacks, fn callback -> callback.(stats) end)
{stats, modules_to_restore}
modules_to_restore
end

defp configure(opts, manager, runner_pid, stats_pid) do
Expand All @@ -88,7 +99,8 @@ defmodule ExUnit.Runner do
seed: opts[:seed],
stats_pid: stats_pid,
timeout: opts[:timeout],
trace: opts[:trace]
trace: opts[:trace],
dry_run: opts[:dry_run]
}
end

Expand Down
10 changes: 8 additions & 2 deletions lib/mix/lib/mix/tasks/test.ex
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ defmodule Mix.Tasks.Test do

* `--cover` - runs coverage tool. See "Coverage" section below

* `--dry-run` *(since v1.19.0)* - prints which tests would be run based on current options,
but does not actually run any tests. This combines with all other options
like `--stale`, `--only`, `--exclude`, and so on.

* `--exclude` - excludes tests that match the filter. This option may be given
several times to apply different filters, such as `--exclude ci --exclude slow`

Expand Down Expand Up @@ -494,7 +498,8 @@ defmodule Mix.Tasks.Test do
warnings_as_errors: :boolean,
profile_require: :string,
exit_status: :integer,
repeat_until_failure: :integer
repeat_until_failure: :integer,
dry_run: :boolean
]

@cover [output: "cover", tool: Mix.Tasks.Test.Coverage]
Expand Down Expand Up @@ -847,7 +852,8 @@ defmodule Mix.Tasks.Test do
:only_test_ids,
:test_location_relative_path,
:exit_status,
:repeat_until_failure
:repeat_until_failure,
:dry_run
]

@doc false
Expand Down
36 changes: 36 additions & 0 deletions lib/mix/test/mix/tasks/test_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,42 @@ defmodule Mix.Tasks.TestTest do
end
end

# TODO: Get to pass after deciding on implementation
@tag :skip
describe "--dry-run" do
Copy link
Contributor Author

@Nezteb Nezteb May 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More test cases to add:

  • --dry-run + --only
  • --dry-run + --exclude
  • ...?

test "prints which tests would run without executing them" do
in_fixture("test_stale", fn ->
File.write!("test/dry_run_test.exs", """
defmodule DryRunTest do
use ExUnit.Case

test "passing test" do
assert true
end

test "failing test" do
assert false
end
end
""")

assert {output, 0} = mix_code(["test", "--dry-run", "--stale"])
assert output =~ "DRY RUN"
assert output =~ "test/dry_run_test.exs"
refute output =~ "Finished in"
end)
end

test "prints message when no tests would run" do
in_fixture("test_stale", fn ->
assert {output, 0} = mix_code(["test", "--dry-run", "non_existent_test.exs"])
assert output =~ "DRY RUN"
assert output =~ "No tests would run"
refute output =~ "Finished in"
end)
end
end

defp receive_until_match(port, expected, acc) do
receive do
{^port, {:data, output}} ->
Expand Down