Skip to content

Commit d78ef17

Browse files
authored
Do not run async groups on load and support --repeat-until-failure (#14107)
1 parent 462f614 commit d78ef17

File tree

3 files changed

+72
-59
lines changed

3 files changed

+72
-59
lines changed

lib/ex_unit/lib/ex_unit/runner.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ defmodule ExUnit.Runner do
125125

126126
# Run all sync modules directly
127127
for pair <- sync_modules do
128-
running = spawn_modules(config, [[pair]], false, %{})
128+
running = spawn_modules(config, [{nil, [pair]}], false, %{})
129129
running != %{} and wait_until_available(config, running)
130130
end
131131

@@ -161,7 +161,7 @@ defmodule ExUnit.Runner do
161161
running
162162
end
163163

164-
defp spawn_modules(config, [[_ | _] = modules | groups], async?, running) do
164+
defp spawn_modules(config, [{_group, [_ | _] = modules} | groups], async?, running) do
165165
if max_failures_reached?(config) do
166166
running
167167
else

lib/ex_unit/lib/ex_unit/server.ex

Lines changed: 52 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,10 @@ defmodule ExUnit.Server do
5757
state = %{
5858
loaded: System.monotonic_time(),
5959
waiting: nil,
60-
async_groups: %{},
60+
groups: %{},
61+
async_groups: [],
6162
async_modules: :queue.new(),
62-
sync_modules: :queue.new()
63+
sync_modules: []
6364
}
6465

6566
{:ok, state}
@@ -72,31 +73,31 @@ defmodule ExUnit.Server do
7273

7374
# Called once after all async modules have been sent and reverts the state.
7475
def handle_call(:take_sync_modules, _from, state) do
75-
%{waiting: nil, loaded: :done, async_modules: async_modules} = state
76-
0 = :queue.len(async_modules)
76+
%{waiting: nil, loaded: :done, async_groups: []} = state
77+
true = :queue.is_empty(state.async_modules)
7778

78-
{:reply, :queue.to_list(state.sync_modules),
79-
%{state | sync_modules: :queue.new(), loaded: System.monotonic_time()}}
79+
{:reply, state.sync_modules, %{state | sync_modules: [], loaded: System.monotonic_time()}}
8080
end
8181

8282
# Called by the runner when --repeat-until-failure is used.
8383
def handle_call({:restore_modules, async_modules, sync_modules}, _from, state) do
84-
{async_modules, async_groups} =
85-
Enum.map_reduce(async_modules, %{}, fn
86-
{nil, [module]}, {modules, groups} ->
87-
{[{:module, module} | modules], groups}
84+
{async_modules, async_groups, groups} =
85+
Enum.reduce(async_modules, {[], [], []}, fn
86+
{nil, [module]}, {async_modules, async_groups, groups} ->
87+
{[module | async_modules], async_groups, groups}
8888

89-
{group, group_modules}, {modules, groups} ->
90-
{[{:group, group} | modules], Map.put(groups, group, group_modules)}
89+
{group, group_modules}, {async_modules, async_groups, groups} ->
90+
{async_modules, [group | async_groups], [{group, group_modules} | groups]}
9191
end)
9292

9393
{:reply, :ok,
9494
%{
9595
state
9696
| loaded: :done,
97+
groups: Map.new(groups),
9798
async_groups: async_groups,
9899
async_modules: :queue.from_list(async_modules),
99-
sync_modules: :queue.from_list(sync_modules)
100+
sync_modules: sync_modules
100101
}}
101102
end
102103

@@ -108,22 +109,24 @@ defmodule ExUnit.Server do
108109
when is_integer(loaded) do
109110
state =
110111
if uniq? do
111-
async_groups =
112-
Map.new(state.async_groups, fn {group, modules} ->
113-
{group, Enum.uniq(modules)}
114-
end)
115-
112+
groups = Map.new(state.groups, fn {group, modules} -> {group, Enum.uniq(modules)} end)
113+
async_groups = state.async_groups |> Enum.uniq() |> Enum.reverse()
116114
async_modules = :queue.to_list(state.async_modules) |> Enum.uniq() |> :queue.from_list()
117-
sync_modules = :queue.to_list(state.sync_modules) |> Enum.uniq() |> :queue.from_list()
115+
sync_modules = state.sync_modules |> Enum.uniq() |> Enum.reverse()
118116

119117
%{
120118
state
121-
| async_groups: async_groups,
119+
| groups: groups,
120+
async_groups: async_groups,
122121
async_modules: async_modules,
123122
sync_modules: sync_modules
124123
}
125124
else
126-
state
125+
%{
126+
state
127+
| async_groups: Enum.reverse(state.async_groups),
128+
sync_modules: Enum.reverse(state.sync_modules)
129+
}
127130
end
128131

129132
diff = System.convert_time_unit(System.monotonic_time() - loaded, :native, :microsecond)
@@ -132,9 +135,7 @@ defmodule ExUnit.Server do
132135

133136
def handle_call({:add, false = _async, _group, names}, _from, %{loaded: loaded} = state)
134137
when is_integer(loaded) do
135-
state =
136-
update_in(state.sync_modules, &Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end))
137-
138+
state = update_in(state.sync_modules, &Enum.reverse(names, &1))
138139
{:reply, :ok, state}
139140
end
140141

@@ -143,25 +144,24 @@ defmodule ExUnit.Server do
143144
state =
144145
update_in(
145146
state.async_modules,
146-
&Enum.reduce(names, &1, fn name, q -> :queue.in({:module, name}, q) end)
147+
&Enum.reduce(names, &1, fn name, q -> :queue.in(name, q) end)
147148
)
148149

149150
{:reply, :ok, take_modules(state)}
150151
end
151152

152153
def handle_call({:add, true = _async, group, names}, _from, %{loaded: loaded} = state)
153154
when is_integer(loaded) do
154-
{async_groups, async_modules} =
155-
case state.async_groups do
156-
%{^group => entries} = async_groups ->
157-
{%{async_groups | group => names ++ entries}, state.async_modules}
155+
{groups, async_groups} =
156+
case state.groups do
157+
%{^group => entries} = groups ->
158+
{%{groups | group => Enum.reverse(names, entries)}, state.async_groups}
158159

159-
%{} = async_groups ->
160-
{Map.put(async_groups, group, names), :queue.in({:group, group}, state.async_modules)}
160+
%{} = groups ->
161+
{Map.put(groups, group, names), [group | state.async_groups]}
161162
end
162163

163-
{:reply, :ok,
164-
take_modules(%{state | async_groups: async_groups, async_modules: async_modules})}
164+
{:reply, :ok, take_modules(%{state | groups: groups, async_groups: async_groups})}
165165
end
166166

167167
def handle_call({:add, _async?, _group, _names}, _from, state) do
@@ -174,49 +174,44 @@ defmodule ExUnit.Server do
174174

175175
defp take_modules(%{waiting: {from, count}} = state) do
176176
has_async_modules? = not :queue.is_empty(state.async_modules)
177+
has_async_groups? = state.async_groups != []
177178

178179
cond do
179-
not has_async_modules? and state.loaded == :done ->
180+
not has_async_modules? and not has_async_groups? and state.loaded == :done ->
180181
GenServer.reply(from, nil)
181182
%{state | waiting: nil}
182183

183-
not has_async_modules? ->
184-
state
185-
186-
true ->
187-
{async_modules, remaining_modules} = take_until(count, state.async_modules)
184+
has_async_modules? ->
185+
{reply, remaining_modules} = take_until(count, state.async_modules)
186+
GenServer.reply(from, reply)
187+
%{state | async_modules: remaining_modules, waiting: nil}
188188

189-
{async_modules, remaining_groups} =
190-
Enum.map_reduce(async_modules, state.async_groups, fn
191-
{:module, module}, async_groups ->
192-
{[module], async_groups}
189+
has_async_groups? ->
190+
{groups, remaining_groups} = Enum.split(state.async_groups, count)
193191

194-
{:group, group}, async_groups ->
195-
{group_modules, async_groups} = Map.pop!(async_groups, group)
196-
{Enum.reverse(group_modules), async_groups}
192+
{reply, groups} =
193+
Enum.map_reduce(groups, state.groups, fn group, acc ->
194+
{entries, acc} = Map.pop!(acc, group)
195+
{{group, Enum.reverse(entries)}, acc}
197196
end)
198197

199-
GenServer.reply(from, async_modules)
198+
GenServer.reply(from, reply)
199+
%{state | groups: groups, async_groups: remaining_groups, waiting: nil}
200200

201-
%{
202-
state
203-
| async_groups: remaining_groups,
204-
async_modules: remaining_modules,
205-
waiting: nil
206-
}
201+
true ->
202+
state
207203
end
208204
end
209205

210-
# :queue.split fails if the provided count is larger than the queue size;
211-
# as we also want to return the values as a list later, we directly
212-
# return {list, queue} instead of {queue, queue}
206+
# :queue.split fails if the provided count is larger than the queue size.
207+
# We also want to return the values as tuples of shape {group, [modules]}.
213208
defp take_until(n, queue), do: take_until(n, queue, [])
214209

215210
defp take_until(0, queue, acc), do: {Enum.reverse(acc), queue}
216211

217212
defp take_until(n, queue, acc) do
218213
case :queue.out(queue) do
219-
{{:value, item}, queue} -> take_until(n - 1, queue, [item | acc])
214+
{{:value, item}, queue} -> take_until(n - 1, queue, [{nil, [item]} | acc])
220215
{:empty, queue} -> {Enum.reverse(acc), queue}
221216
end
222217
end

lib/ex_unit/test/ex_unit_test.exs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,24 @@ defmodule ExUnitTest do
997997
assert length(runs) == 6
998998
end
999999

1000+
test "repeats tests up to the configured number of times with groups" do
1001+
defmodule TestGroupedRepeatUntilFailureReached do
1002+
use ExUnit.Case, async: true, group: :example
1003+
test __ENV__.line, do: assert(true)
1004+
end
1005+
1006+
configure_and_reload_on_exit(repeat_until_failure: 5)
1007+
1008+
output =
1009+
capture_io(fn ->
1010+
assert ExUnit.run() == %{total: 1, failures: 0, skipped: 0, excluded: 0}
1011+
end)
1012+
1013+
runs = String.split(output, "Running ExUnit", trim: true)
1014+
# 6 runs in total, 5 repeats
1015+
assert length(runs) == 6
1016+
end
1017+
10001018
test "stops on failure" do
10011019
{:ok, pid} = Agent.start_link(fn -> 0 end)
10021020
Process.register(pid, :ex_unit_repeat_until_failure_count)

0 commit comments

Comments
 (0)